@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.
- package/CHANGELOG.md +1997 -0
- package/README.md +1160 -0
- package/package.json +135 -0
- package/src/api-registry.ts +95 -0
- package/src/auth-storage.ts +2694 -0
- package/src/cli.ts +493 -0
- package/src/index.ts +42 -0
- package/src/model-cache.ts +97 -0
- package/src/model-manager.ts +349 -0
- package/src/model-thinking.ts +561 -0
- package/src/models.json +49439 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +56 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +81 -0
- package/src/provider-models/descriptors.ts +285 -0
- package/src/provider-models/google.ts +90 -0
- package/src/provider-models/index.ts +4 -0
- package/src/provider-models/openai-compat.ts +2074 -0
- package/src/provider-models/special.ts +106 -0
- package/src/providers/amazon-bedrock.ts +706 -0
- package/src/providers/anthropic.ts +1682 -0
- package/src/providers/azure-openai-responses.ts +391 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2218 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +381 -0
- package/src/providers/google-gemini-cli.ts +1133 -0
- package/src/providers/google-shared.ts +354 -0
- package/src/providers/google-vertex.ts +436 -0
- package/src/providers/google.ts +381 -0
- package/src/providers/kimi.ts +151 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +158 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +2345 -0
- package/src/providers/openai-completions-compat.ts +159 -0
- package/src/providers/openai-completions.ts +1290 -0
- package/src/providers/openai-responses-shared.ts +452 -0
- package/src/providers/openai-responses.ts +519 -0
- package/src/providers/register-builtins.ts +329 -0
- package/src/providers/synthetic.ts +154 -0
- package/src/providers/transform-messages.ts +234 -0
- package/src/rate-limit-utils.ts +84 -0
- package/src/stream.ts +728 -0
- package/src/types.ts +546 -0
- package/src/usage/claude.ts +337 -0
- package/src/usage/gemini.ts +248 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +200 -0
- package/src/usage/kimi.ts +286 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +387 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +130 -0
- package/src/utils/abort.ts +36 -0
- package/src/utils/anthropic-auth.ts +293 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +5 -0
- package/src/utils/discovery/openai-compatible.ts +224 -0
- package/src/utils/event-stream.ts +209 -0
- package/src/utils/http-inspector.ts +165 -0
- package/src/utils/idle-iterator.ts +176 -0
- package/src/utils/json-parse.ts +28 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +134 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +276 -0
- package/src/utils/oauth/cerebras.ts +59 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/github-copilot.ts +358 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/google-antigravity.ts +275 -0
- package/src/utils/oauth/google-gemini-cli.ts +334 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +512 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +251 -0
- package/src/utils/oauth/litellm.ts +81 -0
- package/src/utils/oauth/lm-studio.ts +40 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +59 -0
- package/src/utils/oauth/nanogpt.ts +51 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +190 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +200 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/synthetic.ts +60 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +59 -0
- package/src/utils/oauth/types.ts +89 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +40 -0
- package/src/utils/oauth/xiaomi.ts +88 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +51 -0
- package/src/utils/overflow.ts +134 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +93 -0
- package/src/utils/schema/CONSTRAINTS.md +160 -0
- package/src/utils/schema/adapt.ts +20 -0
- package/src/utils/schema/compatibility.ts +397 -0
- package/src/utils/schema/dereference.ts +93 -0
- package/src/utils/schema/equality.ts +93 -0
- package/src/utils/schema/fields.ts +147 -0
- package/src/utils/schema/index.ts +9 -0
- package/src/utils/schema/normalize-cca.ts +479 -0
- package/src/utils/schema/sanitize-google.ts +212 -0
- package/src/utils/schema/strict-mode.ts +385 -0
- package/src/utils/schema/types.ts +5 -0
- package/src/utils/tool-choice.ts +81 -0
- package/src/utils/validation.ts +664 -0
- package/src/utils.ts +147 -0
package/src/usage/zai.ts
ADDED
|
@@ -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
|
+
}
|