@f5-sales-demo/pi-ai 19.51.2

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 (128) hide show
  1. package/CHANGELOG.md +1997 -0
  2. package/README.md +1160 -0
  3. package/package.json +135 -0
  4. package/src/api-registry.ts +95 -0
  5. package/src/auth-storage.ts +2694 -0
  6. package/src/cli.ts +493 -0
  7. package/src/index.ts +42 -0
  8. package/src/model-cache.ts +97 -0
  9. package/src/model-manager.ts +349 -0
  10. package/src/model-thinking.ts +561 -0
  11. package/src/models.json +49439 -0
  12. package/src/models.json.d.ts +9 -0
  13. package/src/models.ts +56 -0
  14. package/src/prompts/turn-aborted-guidance.md +4 -0
  15. package/src/provider-details.ts +81 -0
  16. package/src/provider-models/descriptors.ts +285 -0
  17. package/src/provider-models/google.ts +90 -0
  18. package/src/provider-models/index.ts +4 -0
  19. package/src/provider-models/openai-compat.ts +2074 -0
  20. package/src/provider-models/special.ts +106 -0
  21. package/src/providers/amazon-bedrock.ts +706 -0
  22. package/src/providers/anthropic.ts +1682 -0
  23. package/src/providers/azure-openai-responses.ts +391 -0
  24. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  25. package/src/providers/cursor/proto/agent.proto +3526 -0
  26. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  27. package/src/providers/cursor/proto/buf.yaml +17 -0
  28. package/src/providers/cursor.ts +2218 -0
  29. package/src/providers/github-copilot-headers.ts +140 -0
  30. package/src/providers/gitlab-duo.ts +381 -0
  31. package/src/providers/google-gemini-cli.ts +1133 -0
  32. package/src/providers/google-shared.ts +354 -0
  33. package/src/providers/google-vertex.ts +436 -0
  34. package/src/providers/google.ts +381 -0
  35. package/src/providers/kimi.ts +151 -0
  36. package/src/providers/openai-codex/constants.ts +43 -0
  37. package/src/providers/openai-codex/request-transformer.ts +158 -0
  38. package/src/providers/openai-codex/response-handler.ts +81 -0
  39. package/src/providers/openai-codex-responses.ts +2345 -0
  40. package/src/providers/openai-completions-compat.ts +159 -0
  41. package/src/providers/openai-completions.ts +1290 -0
  42. package/src/providers/openai-responses-shared.ts +452 -0
  43. package/src/providers/openai-responses.ts +519 -0
  44. package/src/providers/register-builtins.ts +329 -0
  45. package/src/providers/synthetic.ts +154 -0
  46. package/src/providers/transform-messages.ts +234 -0
  47. package/src/rate-limit-utils.ts +84 -0
  48. package/src/stream.ts +728 -0
  49. package/src/types.ts +546 -0
  50. package/src/usage/claude.ts +337 -0
  51. package/src/usage/gemini.ts +248 -0
  52. package/src/usage/github-copilot.ts +421 -0
  53. package/src/usage/google-antigravity.ts +200 -0
  54. package/src/usage/kimi.ts +286 -0
  55. package/src/usage/minimax-code.ts +31 -0
  56. package/src/usage/openai-codex.ts +387 -0
  57. package/src/usage/zai.ts +247 -0
  58. package/src/usage.ts +130 -0
  59. package/src/utils/abort.ts +36 -0
  60. package/src/utils/anthropic-auth.ts +293 -0
  61. package/src/utils/discovery/antigravity.ts +261 -0
  62. package/src/utils/discovery/codex.ts +371 -0
  63. package/src/utils/discovery/cursor.ts +306 -0
  64. package/src/utils/discovery/gemini.ts +248 -0
  65. package/src/utils/discovery/index.ts +5 -0
  66. package/src/utils/discovery/openai-compatible.ts +224 -0
  67. package/src/utils/event-stream.ts +209 -0
  68. package/src/utils/http-inspector.ts +165 -0
  69. package/src/utils/idle-iterator.ts +176 -0
  70. package/src/utils/json-parse.ts +28 -0
  71. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  72. package/src/utils/oauth/anthropic.ts +134 -0
  73. package/src/utils/oauth/api-key-validation.ts +92 -0
  74. package/src/utils/oauth/callback-server.ts +276 -0
  75. package/src/utils/oauth/cerebras.ts +59 -0
  76. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  77. package/src/utils/oauth/cursor.ts +157 -0
  78. package/src/utils/oauth/github-copilot.ts +358 -0
  79. package/src/utils/oauth/gitlab-duo.ts +123 -0
  80. package/src/utils/oauth/google-antigravity.ts +275 -0
  81. package/src/utils/oauth/google-gemini-cli.ts +334 -0
  82. package/src/utils/oauth/huggingface.ts +62 -0
  83. package/src/utils/oauth/index.ts +512 -0
  84. package/src/utils/oauth/kagi.ts +47 -0
  85. package/src/utils/oauth/kilo.ts +87 -0
  86. package/src/utils/oauth/kimi.ts +251 -0
  87. package/src/utils/oauth/litellm.ts +81 -0
  88. package/src/utils/oauth/lm-studio.ts +40 -0
  89. package/src/utils/oauth/minimax-code.ts +78 -0
  90. package/src/utils/oauth/moonshot.ts +59 -0
  91. package/src/utils/oauth/nanogpt.ts +51 -0
  92. package/src/utils/oauth/nvidia.ts +70 -0
  93. package/src/utils/oauth/oauth.html +199 -0
  94. package/src/utils/oauth/ollama.ts +47 -0
  95. package/src/utils/oauth/openai-codex.ts +190 -0
  96. package/src/utils/oauth/opencode.ts +49 -0
  97. package/src/utils/oauth/parallel.ts +46 -0
  98. package/src/utils/oauth/perplexity.ts +200 -0
  99. package/src/utils/oauth/pkce.ts +18 -0
  100. package/src/utils/oauth/qianfan.ts +58 -0
  101. package/src/utils/oauth/qwen-portal.ts +60 -0
  102. package/src/utils/oauth/synthetic.ts +60 -0
  103. package/src/utils/oauth/tavily.ts +46 -0
  104. package/src/utils/oauth/together.ts +59 -0
  105. package/src/utils/oauth/types.ts +89 -0
  106. package/src/utils/oauth/venice.ts +59 -0
  107. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  108. package/src/utils/oauth/vllm.ts +40 -0
  109. package/src/utils/oauth/xiaomi.ts +88 -0
  110. package/src/utils/oauth/zai.ts +60 -0
  111. package/src/utils/oauth/zenmux.ts +51 -0
  112. package/src/utils/overflow.ts +134 -0
  113. package/src/utils/retry-after.ts +110 -0
  114. package/src/utils/retry.ts +93 -0
  115. package/src/utils/schema/CONSTRAINTS.md +160 -0
  116. package/src/utils/schema/adapt.ts +20 -0
  117. package/src/utils/schema/compatibility.ts +397 -0
  118. package/src/utils/schema/dereference.ts +93 -0
  119. package/src/utils/schema/equality.ts +93 -0
  120. package/src/utils/schema/fields.ts +147 -0
  121. package/src/utils/schema/index.ts +9 -0
  122. package/src/utils/schema/normalize-cca.ts +479 -0
  123. package/src/utils/schema/sanitize-google.ts +212 -0
  124. package/src/utils/schema/strict-mode.ts +385 -0
  125. package/src/utils/schema/types.ts +5 -0
  126. package/src/utils/tool-choice.ts +81 -0
  127. package/src/utils/validation.ts +664 -0
  128. package/src/utils.ts +147 -0
@@ -0,0 +1,247 @@
1
+ import type {
2
+ UsageAmount,
3
+ UsageFetchContext,
4
+ UsageFetchParams,
5
+ UsageLimit,
6
+ UsageProvider,
7
+ UsageReport,
8
+ UsageStatus,
9
+ UsageWindow,
10
+ } from "../usage";
11
+ import { isRecord, toNumber } from "../utils";
12
+
13
+ const DEFAULT_ENDPOINT = "https://api.z.ai";
14
+ const QUOTA_PATH = "/api/monitor/usage/quota/limit";
15
+ const MODEL_USAGE_PATH = "/api/monitor/usage/model-usage";
16
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
17
+
18
+ function normalizeZaiBaseUrl(baseUrl?: string): string {
19
+ if (!baseUrl?.trim()) return DEFAULT_ENDPOINT;
20
+ try {
21
+ return new URL(baseUrl.trim()).origin;
22
+ } catch {
23
+ return DEFAULT_ENDPOINT;
24
+ }
25
+ }
26
+
27
+ interface ZaiUsageLimitItem {
28
+ type?: string;
29
+ usage?: number;
30
+ currentValue?: number;
31
+ percentage?: number;
32
+ remaining?: number;
33
+ nextResetTime?: number;
34
+ }
35
+
36
+ interface ZaiQuotaPayload {
37
+ success?: boolean;
38
+ code?: number;
39
+ msg?: string;
40
+ data?: {
41
+ limits?: ZaiUsageLimitItem[];
42
+ };
43
+ }
44
+
45
+ function parseMillis(value: unknown): number | undefined {
46
+ const parsed = toNumber(value);
47
+ if (parsed === undefined) return undefined;
48
+ return parsed > 1_000_000_000_000 ? parsed : parsed * 1000;
49
+ }
50
+
51
+ function parseLimitItem(value: unknown): ZaiUsageLimitItem | null {
52
+ if (!isRecord(value)) return null;
53
+ const type = typeof value.type === "string" ? value.type : undefined;
54
+ if (!type) return null;
55
+ return {
56
+ type,
57
+ usage: toNumber(value.usage),
58
+ currentValue: toNumber(value.currentValue),
59
+ percentage: toNumber(value.percentage),
60
+ remaining: toNumber(value.remaining),
61
+ nextResetTime: parseMillis(value.nextResetTime),
62
+ };
63
+ }
64
+
65
+ function buildUsageAmount(args: {
66
+ used: number | undefined;
67
+ limit: number | undefined;
68
+ remaining: number | undefined;
69
+ unit: UsageAmount["unit"];
70
+ percentage?: number;
71
+ }): UsageAmount {
72
+ const usedFraction =
73
+ args.percentage !== undefined
74
+ ? Math.min(Math.max(args.percentage / 100, 0), 1)
75
+ : args.used !== undefined && args.limit !== undefined && args.limit > 0
76
+ ? Math.min(args.used / args.limit, 1)
77
+ : undefined;
78
+ const remainingFraction = usedFraction !== undefined ? Math.max(1 - usedFraction, 0) : undefined;
79
+ return {
80
+ used: args.used,
81
+ limit: args.limit,
82
+ remaining: args.remaining,
83
+ usedFraction,
84
+ remainingFraction,
85
+ unit: args.unit,
86
+ };
87
+ }
88
+
89
+ function getUsageStatus(usedFraction: number | undefined): UsageStatus | undefined {
90
+ if (usedFraction === undefined) return undefined;
91
+ if (usedFraction >= 1) return "exhausted";
92
+ if (usedFraction >= 0.9) return "warning";
93
+ return "ok";
94
+ }
95
+
96
+ function formatDate(value: Date): string {
97
+ const pad = (input: number) => String(input).padStart(2, "0");
98
+ return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}+${pad(value.getHours())}:${pad(
99
+ value.getMinutes(),
100
+ )}:${pad(value.getSeconds())}`;
101
+ }
102
+
103
+ function buildModelUsageUrl(baseUrl: string, now: Date): string {
104
+ const start = new Date(now.getTime() - SEVEN_DAYS_MS);
105
+ const startTime = formatDate(start);
106
+ const endTime = formatDate(now);
107
+ return `${baseUrl}${MODEL_USAGE_PATH}?startTime=${encodeURIComponent(startTime)}&endTime=${encodeURIComponent(endTime)}`;
108
+ }
109
+
110
+ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
111
+ if (params.provider !== "zai") return null;
112
+ const credential = params.credential;
113
+ if (credential.type !== "api_key" || !credential.apiKey) return null;
114
+
115
+ const baseUrl = normalizeZaiBaseUrl(params.baseUrl);
116
+ const url = `${baseUrl}${QUOTA_PATH}`;
117
+ const headers: Record<string, string> = {
118
+ Authorization: credential.apiKey,
119
+ "Content-Type": "application/json",
120
+ "User-Agent": "OpenCode-Status-Plugin/1.0",
121
+ };
122
+
123
+ let payload: ZaiQuotaPayload | null = null;
124
+ try {
125
+ const response = await ctx.fetch(url, {
126
+ headers,
127
+ signal: params.signal,
128
+ });
129
+ if (!response.ok) {
130
+ ctx.logger?.warn("ZAI usage fetch failed", { status: response.status, statusText: response.statusText });
131
+ return null;
132
+ }
133
+ payload = (await response.json()) as ZaiQuotaPayload;
134
+ } catch (error) {
135
+ ctx.logger?.warn("ZAI usage fetch error", { error: String(error) });
136
+ return null;
137
+ }
138
+
139
+ if (!payload) return null;
140
+ if (payload.success !== true) {
141
+ ctx.logger?.warn("ZAI usage response invalid", { code: payload.code, message: payload.msg });
142
+ return null;
143
+ }
144
+
145
+ const limitsPayload = Array.isArray(payload.data?.limits) ? payload.data?.limits : [];
146
+ const limits: UsageLimit[] = [];
147
+
148
+ for (const rawLimit of limitsPayload) {
149
+ const parsed = parseLimitItem(rawLimit);
150
+ if (!parsed) continue;
151
+ if (parsed.type === "TOKENS_LIMIT") {
152
+ const amount = buildUsageAmount({
153
+ used: parsed.currentValue,
154
+ limit: parsed.usage,
155
+ remaining: parsed.remaining,
156
+ percentage: parsed.percentage,
157
+ unit: "tokens",
158
+ });
159
+ const window: UsageWindow = {
160
+ id: "quota",
161
+ label: "Quota",
162
+ durationMs: SEVEN_DAYS_MS,
163
+ resetsAt: parsed.nextResetTime,
164
+ };
165
+ limits.push({
166
+ id: "zai:tokens",
167
+ label: "ZAI Token Quota",
168
+ scope: {
169
+ provider: params.provider,
170
+ windowId: window?.id ?? "quota",
171
+ shared: true,
172
+ },
173
+ window,
174
+ amount,
175
+ status: getUsageStatus(amount.usedFraction),
176
+ });
177
+ }
178
+ if (parsed.type === "TIME_LIMIT") {
179
+ const window: UsageWindow = {
180
+ id: "quota",
181
+ label: "Quota",
182
+ durationMs: SEVEN_DAYS_MS,
183
+ resetsAt: parsed.nextResetTime,
184
+ };
185
+ const amount = buildUsageAmount({
186
+ used: parsed.currentValue,
187
+ limit: parsed.usage,
188
+ remaining: parsed.remaining,
189
+ percentage: parsed.percentage,
190
+ unit: "requests",
191
+ });
192
+ limits.push({
193
+ id: "zai:requests",
194
+ label: "ZAI Request Quota",
195
+ scope: {
196
+ provider: params.provider,
197
+ windowId: "quota",
198
+ shared: true,
199
+ },
200
+ window,
201
+ amount,
202
+ status: getUsageStatus(amount.usedFraction),
203
+ });
204
+ }
205
+ }
206
+
207
+ if (limits.length === 0) return null;
208
+
209
+ const report: UsageReport = {
210
+ provider: params.provider,
211
+ fetchedAt: Date.now(),
212
+ limits,
213
+ metadata: {
214
+ endpoint: url,
215
+ accountId: credential.accountId,
216
+ email: credential.email,
217
+ },
218
+ raw: payload,
219
+ };
220
+
221
+ const modelUsageUrl = buildModelUsageUrl(baseUrl, new Date());
222
+ try {
223
+ const response = await ctx.fetch(modelUsageUrl, {
224
+ headers,
225
+ signal: params.signal,
226
+ });
227
+ if (response.ok) {
228
+ const modelUsagePayload = (await response.json()) as unknown;
229
+ if (isRecord(modelUsagePayload)) {
230
+ report.metadata = {
231
+ ...report.metadata,
232
+ modelUsage: modelUsagePayload,
233
+ };
234
+ }
235
+ }
236
+ } catch (error) {
237
+ ctx.logger?.debug("ZAI model usage fetch failed", { error: String(error) });
238
+ }
239
+
240
+ return report;
241
+ }
242
+
243
+ export const zaiUsageProvider: UsageProvider = {
244
+ id: "zai",
245
+ fetchUsage: fetchZaiUsage,
246
+ supports: params => params.provider === "zai" && params.credential.type === "api_key",
247
+ };
package/src/usage.ts ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Usage reporting types for provider quota/limit endpoints.
3
+ *
4
+ * Provides a normalized schema to represent multiple limit windows, model tiers,
5
+ * and shared quotas across providers.
6
+ */
7
+ import type { Provider } from "./types";
8
+
9
+ export type UsageUnit = "percent" | "tokens" | "requests" | "usd" | "minutes" | "bytes" | "unknown";
10
+
11
+ export type UsageStatus = "ok" | "warning" | "exhausted" | "unknown";
12
+
13
+ /** Time window for a limit (e.g. 5h, 7d, monthly). */
14
+ export interface UsageWindow {
15
+ /** Stable identifier (e.g. "5h", "7d", "monthly"). */
16
+ id: string;
17
+ /** Human label (e.g. "5 Hour", "7 Day"). */
18
+ label: string;
19
+ /** Window duration in milliseconds, when known. */
20
+ durationMs?: number;
21
+ /** Absolute reset timestamp in milliseconds since epoch. */
22
+ resetsAt?: number;
23
+ }
24
+
25
+ /** Quantitative usage data. */
26
+ export interface UsageAmount {
27
+ /** Amount used in the given unit. */
28
+ used?: number;
29
+ /** Maximum limit in the given unit. */
30
+ limit?: number;
31
+ /** Remaining amount in the given unit. */
32
+ remaining?: number;
33
+ /** Fraction used (0..1). */
34
+ usedFraction?: number;
35
+ /** Fraction remaining (0..1). */
36
+ remainingFraction?: number;
37
+ /** Unit for the amounts (percent, tokens, etc.). */
38
+ unit: UsageUnit;
39
+ }
40
+
41
+ /** Scope metadata describing what the limit applies to. */
42
+ export interface UsageScope {
43
+ provider: Provider;
44
+ accountId?: string;
45
+ projectId?: string;
46
+ orgId?: string;
47
+ modelId?: string;
48
+ tier?: string;
49
+ windowId?: string;
50
+ shared?: boolean;
51
+ }
52
+
53
+ /** Normalized limit entry for a single window or quota bucket. */
54
+ export interface UsageLimit {
55
+ /** Stable identifier for this limit entry. */
56
+ id: string;
57
+ /** Human label for display. */
58
+ label: string;
59
+ scope: UsageScope;
60
+ window?: UsageWindow;
61
+ amount: UsageAmount;
62
+ status?: UsageStatus;
63
+ notes?: string[];
64
+ }
65
+
66
+ /** Aggregated usage report for a provider. */
67
+ export interface UsageReport {
68
+ provider: Provider;
69
+ fetchedAt: number;
70
+ limits: UsageLimit[];
71
+ metadata?: Record<string, unknown>;
72
+ raw?: unknown;
73
+ }
74
+
75
+ /** Optional logger for usage fetchers. */
76
+ export interface UsageLogger {
77
+ debug(message: string, meta?: Record<string, unknown>): void;
78
+ warn(message: string, meta?: Record<string, unknown>): void;
79
+ }
80
+
81
+ /** Credential bundle for usage endpoints. */
82
+ export interface UsageCredential {
83
+ type: "api_key" | "oauth";
84
+ apiKey?: string;
85
+ accessToken?: string;
86
+ refreshToken?: string;
87
+ expiresAt?: number;
88
+ accountId?: string;
89
+ projectId?: string;
90
+ email?: string;
91
+ enterpriseUrl?: string;
92
+ metadata?: Record<string, unknown>;
93
+ }
94
+
95
+ /** Parameters provided to a usage fetcher. */
96
+ export interface UsageFetchParams {
97
+ provider: Provider;
98
+ credential: UsageCredential;
99
+ baseUrl?: string;
100
+ signal?: AbortSignal;
101
+ }
102
+
103
+ /** Shared runtime utilities for fetchers. */
104
+ export interface UsageFetchContext {
105
+ fetch: typeof fetch;
106
+ logger?: UsageLogger;
107
+ }
108
+
109
+ /** Provider implementation for fetching usage information. */
110
+ export interface UsageProvider {
111
+ id: Provider;
112
+ fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null>;
113
+ supports?(params: UsageFetchParams): boolean;
114
+ }
115
+
116
+ /** Strategy for usage-based credential ranking. Providers implement this to opt into smart credential selection. */
117
+ export interface CredentialRankingStrategy {
118
+ /** Extract the primary (short) and secondary (long) window limits from a usage report. */
119
+ findWindowLimits(report: UsageReport): {
120
+ primary?: UsageLimit;
121
+ secondary?: UsageLimit;
122
+ };
123
+ /** Fallback window durations (ms) when limits don't specify durationMs. */
124
+ windowDefaults: {
125
+ primaryMs: number;
126
+ secondaryMs: number;
127
+ };
128
+ /** Optional: priority boost for specific credential states (e.g., fresh 5h ticker start). */
129
+ hasPriorityBoost?(primary: UsageLimit | undefined): boolean;
130
+ }
@@ -0,0 +1,36 @@
1
+ export interface AbortSourceTracker {
2
+ requestAbortController: AbortController;
3
+ requestSignal: AbortSignal;
4
+ abortLocally(reason: Error): Error;
5
+ getLocalAbortReason(): Error | undefined;
6
+ wasCallerAbort(): boolean;
7
+ }
8
+
9
+ /**
10
+ * Tracks whether a merged request signal was aborted by the caller or by provider-local logic.
11
+ */
12
+ export function createAbortSourceTracker(callerSignal?: AbortSignal): AbortSourceTracker {
13
+ const requestAbortController = new AbortController();
14
+ const requestSignal = callerSignal
15
+ ? AbortSignal.any([callerSignal, requestAbortController.signal])
16
+ : requestAbortController.signal;
17
+ let localAbortReason: Error | undefined;
18
+
19
+ return {
20
+ requestAbortController,
21
+ requestSignal,
22
+ abortLocally(reason) {
23
+ localAbortReason = reason;
24
+ requestAbortController.abort(reason);
25
+ return reason;
26
+ },
27
+ getLocalAbortReason() {
28
+ if (!localAbortReason) return undefined;
29
+ return requestSignal.reason === localAbortReason ? localAbortReason : undefined;
30
+ },
31
+ wasCallerAbort() {
32
+ if (!callerSignal?.aborted) return false;
33
+ return requestSignal.reason !== localAbortReason;
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Anthropic Authentication
3
+ *
4
+ * 7-tier auth resolution:
5
+ * 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL env vars
6
+ * 2. ANTHROPIC_FOUNDRY_API_KEY override when Foundry mode is enabled
7
+ * 3. OAuth credentials in ~/.xcsh/agent/agent.db (with expiry check)
8
+ * 4. API key credentials in ~/.xcsh/agent/agent.db
9
+ * 5. Generic Anthropic fallback (ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL)
10
+ * 6. Anthropic provider credentials from models.yml
11
+ * 7. LiteLLM passthrough (LITELLM_BASE_URL / LITELLM_API_KEY)
12
+ */
13
+ import { $env, getAgentDbPath, readProviderFromModelsYml } from "@f5xc-salesdemos/pi-utils";
14
+ import { type AuthCredential, AuthCredentialStore } from "../auth-storage";
15
+ import {
16
+ buildAnthropicHeaders as buildProviderAnthropicHeaders,
17
+ normalizeAnthropicBaseUrl,
18
+ } from "../providers/anthropic";
19
+ import { getEnvApiKey } from "../stream";
20
+
21
+ /** Auth configuration for Anthropic */
22
+ export interface AnthropicAuthConfig {
23
+ apiKey: string;
24
+ baseUrl: string;
25
+ isOAuth: boolean;
26
+ }
27
+
28
+ /** OAuth credential for Anthropic API access */
29
+ export interface AnthropicOAuthCredential {
30
+ type: "oauth";
31
+ access: string;
32
+ refresh?: string;
33
+ /** Expiry timestamp in milliseconds */
34
+ expires: number;
35
+ }
36
+
37
+ const DEFAULT_BASE_URL = "https://api.anthropic.com";
38
+
39
+ function isFoundryEnabled(): boolean {
40
+ const value = $env.CLAUDE_CODE_USE_FOUNDRY;
41
+ if (!value) return false;
42
+ const normalized = value.trim().toLowerCase();
43
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
44
+ }
45
+
46
+ function normalizeBaseUrl(baseUrl: string | undefined): string | undefined {
47
+ const trimmed = baseUrl?.trim();
48
+ return trimmed ? trimmed.replace(/\/+$/, "") : undefined;
49
+ }
50
+ function resolveAnthropicBaseUrlFromEnv(): string | undefined {
51
+ if (isFoundryEnabled()) {
52
+ const foundryBaseUrl = normalizeBaseUrl($env.FOUNDRY_BASE_URL);
53
+ if (foundryBaseUrl) return foundryBaseUrl;
54
+ }
55
+ const anthropicBaseUrl = normalizeBaseUrl($env.ANTHROPIC_BASE_URL);
56
+ return anthropicBaseUrl || undefined;
57
+ }
58
+
59
+ /**
60
+ * Checks if a token is an OAuth token by looking for sk-ant-oat prefix.
61
+ * @param apiKey - The API key to check
62
+ * @returns True if the token is an OAuth token
63
+ */
64
+ export function isOAuthToken(apiKey: string): boolean {
65
+ return apiKey.includes("sk-ant-oat");
66
+ }
67
+
68
+ /**
69
+ * Converts a generic AuthCredential to AnthropicOAuthCredential if it's a valid OAuth entry.
70
+ * @param credential - The credential to convert
71
+ * @returns The converted OAuth credential, or null if not a valid OAuth type
72
+ */
73
+ function toAnthropicOAuthCredential(credential: AuthCredential): AnthropicOAuthCredential | null {
74
+ if (credential.type !== "oauth") return null;
75
+ if (typeof credential.access !== "string" || typeof credential.expires !== "number") return null;
76
+ return {
77
+ type: "oauth",
78
+ access: credential.access,
79
+ refresh: credential.refresh,
80
+ expires: credential.expires,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Reads Anthropic OAuth credentials from an AuthCredentialStore.
86
+ * @param store - Credential store to read from (creates AuthCredentialStore if not provided)
87
+ * @returns Array of valid Anthropic OAuth credentials
88
+ */
89
+ async function readAnthropicOAuthCredentials(store?: AuthCredentialStore): Promise<AnthropicOAuthCredential[]> {
90
+ const ownsStore = !store;
91
+ const effectiveStore = store ?? (await AuthCredentialStore.open(getAgentDbPath()));
92
+ try {
93
+ const records = effectiveStore.listAuthCredentials("anthropic");
94
+ const credentials: AnthropicOAuthCredential[] = [];
95
+ for (const record of records) {
96
+ const mapped = toAnthropicOAuthCredential(record.credential);
97
+ if (mapped) {
98
+ credentials.push(mapped);
99
+ }
100
+ }
101
+
102
+ return credentials;
103
+ } finally {
104
+ if (ownsStore) {
105
+ effectiveStore.close();
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Resolves Anthropic provider credentials from models.yml (written by LiteLLM auto-config).
112
+ * Skips shell-backed secrets, which this tier cannot resolve.
113
+ * @returns AnthropicAuthConfig or null if unavailable
114
+ */
115
+ function readAnthropicAuthFromModelsYml(): AnthropicAuthConfig | null {
116
+ const block = readProviderFromModelsYml("anthropic");
117
+ if (!block?.baseUrl || !block.apiKey) return null;
118
+
119
+ let apiKey: string | undefined;
120
+ switch (block.apiKey.kind) {
121
+ case "envVar":
122
+ apiKey = process.env[block.apiKey.name];
123
+ break;
124
+ case "literal":
125
+ apiKey = block.apiKey.value;
126
+ break;
127
+ case "shellSecret":
128
+ return null;
129
+ }
130
+
131
+ if (!apiKey) return null;
132
+ return { apiKey, baseUrl: block.baseUrl, isOAuth: false };
133
+ }
134
+
135
+ /**
136
+ * Strips known suffixes (`/` repeats, `/anthropic`, `/api/v1`, `/v1`) from a
137
+ * LiteLLM base URL until no further suffix matches. Handles inputs like
138
+ * `https://proxy/api/v1/anthropic` in any order.
139
+ *
140
+ * Exported for contract testing — see packages/ai/test/litellm-url-normalization.test.ts.
141
+ */
142
+ export function normalizeLitellmBase(url: string): string {
143
+ const suffixes = [/\/+$/, /\/anthropic$/, /\/api\/v\d+$/, /\/v\d+$/];
144
+ let prev: string;
145
+ let current = url;
146
+ do {
147
+ prev = current;
148
+ for (const re of suffixes) current = current.replace(re, "");
149
+ } while (current !== prev);
150
+ return current;
151
+ }
152
+
153
+ /**
154
+ * Finds Anthropic auth config using priority:
155
+ * 1. ANTHROPIC_SEARCH_API_KEY / ANTHROPIC_SEARCH_BASE_URL
156
+ * 2. ANTHROPIC_FOUNDRY_API_KEY override when Foundry mode is enabled
157
+ * 3. OAuth in agent.db (with 5-minute expiry buffer)
158
+ * 4. API key in agent.db
159
+ * 5. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
160
+ * 6. Anthropic provider credentials from models.yml
161
+ * 7. LiteLLM passthrough (LITELLM_BASE_URL / LITELLM_API_KEY)
162
+ * @param store - Optional credential store (creates one from default db path if not provided)
163
+ * @returns The first valid auth configuration found, or null if none available
164
+ */
165
+ export async function findAnthropicAuth(store?: AuthCredentialStore): Promise<AnthropicAuthConfig | null> {
166
+ // 1. Explicit search-specific env vars
167
+ const searchApiKey = $env.ANTHROPIC_SEARCH_API_KEY;
168
+ const searchBaseUrl = $env.ANTHROPIC_SEARCH_BASE_URL;
169
+ if (searchApiKey) {
170
+ return {
171
+ apiKey: searchApiKey,
172
+ baseUrl: searchBaseUrl ?? DEFAULT_BASE_URL,
173
+ isOAuth: isOAuthToken(searchApiKey),
174
+ };
175
+ }
176
+
177
+ // 2. Foundry explicit env override
178
+ const foundryApiKey = isFoundryEnabled() ? $env.ANTHROPIC_FOUNDRY_API_KEY?.trim() : undefined;
179
+ if (foundryApiKey) {
180
+ return {
181
+ apiKey: foundryApiKey,
182
+ baseUrl: resolveAnthropicBaseUrlFromEnv() ?? DEFAULT_BASE_URL,
183
+ isOAuth: isOAuthToken(foundryApiKey),
184
+ };
185
+ }
186
+
187
+ // Tiers 3-4 use the credential store; wrapped in try/catch so DB
188
+ // failures (missing file, corruption, locking) never prevent the
189
+ // env-var fallback tiers from running.
190
+ let storeError: unknown;
191
+ try {
192
+ const ownsStore = !store;
193
+ const effectiveStore = store ?? (await AuthCredentialStore.open(getAgentDbPath()));
194
+ try {
195
+ // 3. OAuth credentials in agent.db (with 5-minute expiry buffer)
196
+ const expiryBuffer = 5 * 60 * 1000; // 5 minutes
197
+ const now = Date.now();
198
+ const credentials = await readAnthropicOAuthCredentials(effectiveStore);
199
+ for (const credential of credentials) {
200
+ if (!credential.access) continue;
201
+ if (credential.expires > now + expiryBuffer) {
202
+ return {
203
+ apiKey: credential.access,
204
+ baseUrl: DEFAULT_BASE_URL,
205
+ isOAuth: true,
206
+ };
207
+ }
208
+ }
209
+
210
+ // 4. API key credentials in agent.db
211
+ const storedApiKey = effectiveStore.getApiKey("anthropic");
212
+ if (storedApiKey) {
213
+ return {
214
+ apiKey: storedApiKey,
215
+ baseUrl: resolveAnthropicBaseUrlFromEnv() ?? DEFAULT_BASE_URL,
216
+ isOAuth: isOAuthToken(storedApiKey),
217
+ };
218
+ }
219
+ } finally {
220
+ if (ownsStore) {
221
+ effectiveStore.close();
222
+ }
223
+ }
224
+ } catch (err) {
225
+ // DB unavailable — fall through to env-var tiers, but preserve the
226
+ // error so it can be surfaced if no later tier succeeds.
227
+ storeError = err;
228
+ }
229
+
230
+ // 5. Generic ANTHROPIC_API_KEY fallback
231
+ const apiKey = getEnvApiKey("anthropic");
232
+ const baseUrl = resolveAnthropicBaseUrlFromEnv();
233
+ if (apiKey) {
234
+ return {
235
+ apiKey,
236
+ baseUrl: baseUrl ?? DEFAULT_BASE_URL,
237
+ isOAuth: isOAuthToken(apiKey),
238
+ };
239
+ }
240
+
241
+ // 6. Anthropic provider credentials from models.yml
242
+ try {
243
+ const modelsYmlAuth = readAnthropicAuthFromModelsYml();
244
+ if (modelsYmlAuth) return modelsYmlAuth;
245
+ } catch {
246
+ /* Best-effort — don't block env-var fallback */
247
+ }
248
+
249
+ // 7. Derive from litellm passthrough credentials
250
+ const litellmBaseUrl = $env.LITELLM_BASE_URL;
251
+ const litellmApiKey = $env.LITELLM_API_KEY;
252
+ if (litellmBaseUrl && litellmApiKey) {
253
+ return {
254
+ apiKey: litellmApiKey,
255
+ baseUrl: `${normalizeLitellmBase(litellmBaseUrl)}/anthropic`,
256
+ isOAuth: false,
257
+ };
258
+ }
259
+
260
+ // No auth tier succeeded. If the credential store threw, surface
261
+ // that error so a broken DB isn't silently hidden as "unconfigured".
262
+ if (storeError) {
263
+ throw storeError;
264
+ }
265
+
266
+ return null;
267
+ }
268
+
269
+ /**
270
+ * Builds HTTP headers for Anthropic API requests (search variant).
271
+ * @param auth - The authentication configuration
272
+ * @returns Headers object ready for use in fetch requests
273
+ */
274
+ export function buildAnthropicSearchHeaders(auth: AnthropicAuthConfig): Record<string, string> {
275
+ return buildProviderAnthropicHeaders({
276
+ apiKey: auth.apiKey,
277
+ baseUrl: auth.baseUrl,
278
+ isOAuth: auth.isOAuth,
279
+ extraBetas: ["web-search-2025-03-05"],
280
+ stream: false,
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Builds the full API URL for Anthropic messages endpoint.
286
+ * @param auth - The authentication configuration
287
+ * @returns The complete API URL with beta query parameter
288
+ */
289
+ export function buildAnthropicUrl(auth: AnthropicAuthConfig): string {
290
+ const normalizedBaseUrl = normalizeAnthropicBaseUrl(auth.baseUrl);
291
+ const base = `${normalizedBaseUrl}/v1/messages`;
292
+ return `${base}?beta=true`;
293
+ }