@sriinnu/kosha-discovery 1.1.0 → 1.5.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.
- package/README.md +66 -26
- package/dist/aliases.d.ts +6 -1
- package/dist/aliases.d.ts.map +1 -1
- package/dist/aliases.js +38 -12
- package/dist/aliases.js.map +1 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +25 -2
- package/dist/cache.js.map +1 -1
- package/dist/claude-generation.d.ts +59 -0
- package/dist/claude-generation.d.ts.map +1 -0
- package/dist/claude-generation.js +117 -0
- package/dist/claude-generation.js.map +1 -0
- package/dist/cli-cmd-doctor.d.ts +17 -0
- package/dist/cli-cmd-doctor.d.ts.map +1 -0
- package/dist/cli-cmd-doctor.js +199 -0
- package/dist/cli-cmd-doctor.js.map +1 -0
- package/dist/cli-cmd-model.js +2 -2
- package/dist/cli-cmd-model.js.map +1 -1
- package/dist/cli-cmd-spend.d.ts +10 -0
- package/dist/cli-cmd-spend.d.ts.map +1 -0
- package/dist/cli-cmd-spend.js +130 -0
- package/dist/cli-cmd-spend.js.map +1 -0
- package/dist/cli-commands.d.ts +4 -1
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +10 -2
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli-format.d.ts.map +1 -1
- package/dist/cli-format.js +2 -1
- package/dist/cli-format.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +4 -2
- package/dist/cli-help.js.map +1 -1
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/context-strategy.d.ts +48 -0
- package/dist/context-strategy.d.ts.map +1 -0
- package/dist/context-strategy.js +274 -0
- package/dist/context-strategy.js.map +1 -0
- package/dist/cost.d.ts +152 -0
- package/dist/cost.d.ts.map +1 -0
- package/dist/cost.js +377 -0
- package/dist/cost.js.map +1 -0
- package/dist/credentials/resolver.d.ts +17 -0
- package/dist/credentials/resolver.d.ts.map +1 -1
- package/dist/credentials/resolver.js +92 -34
- package/dist/credentials/resolver.js.map +1 -1
- package/dist/discovery/anthropic.d.ts +19 -1
- package/dist/discovery/anthropic.d.ts.map +1 -1
- package/dist/discovery/anthropic.js +112 -8
- package/dist/discovery/anthropic.js.map +1 -1
- package/dist/discovery/base.d.ts +7 -0
- package/dist/discovery/base.d.ts.map +1 -1
- package/dist/discovery/base.js +32 -4
- package/dist/discovery/base.js.map +1 -1
- package/dist/discovery/bedrock.d.ts.map +1 -1
- package/dist/discovery/cerebras.d.ts.map +1 -1
- package/dist/discovery/cohere.d.ts.map +1 -1
- package/dist/discovery/deepinfra.d.ts.map +1 -1
- package/dist/discovery/deepseek.d.ts.map +1 -1
- package/dist/discovery/fireworks.d.ts.map +1 -1
- package/dist/discovery/glm.d.ts.map +1 -1
- package/dist/discovery/google.d.ts +3 -1
- package/dist/discovery/google.d.ts.map +1 -1
- package/dist/discovery/google.js +13 -6
- package/dist/discovery/google.js.map +1 -1
- package/dist/discovery/groq.d.ts.map +1 -1
- package/dist/discovery/index.d.ts +3 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +9 -0
- package/dist/discovery/index.js.map +1 -1
- package/dist/discovery/litellm-seed.js +12 -8
- package/dist/discovery/litellm-seed.js.map +1 -1
- package/dist/discovery/llama-cpp.d.ts.map +1 -1
- package/dist/discovery/lmstudio.d.ts +26 -0
- package/dist/discovery/lmstudio.d.ts.map +1 -0
- package/dist/discovery/lmstudio.js +93 -0
- package/dist/discovery/lmstudio.js.map +1 -0
- package/dist/discovery/minimax.d.ts.map +1 -1
- package/dist/discovery/mistral.d.ts.map +1 -1
- package/dist/discovery/modelsdev-seed.js +16 -12
- package/dist/discovery/modelsdev-seed.js.map +1 -1
- package/dist/discovery/moonshot.d.ts.map +1 -1
- package/dist/discovery/nvidia.d.ts.map +1 -1
- package/dist/discovery/ollama.d.ts.map +1 -1
- package/dist/discovery/openai-compatible.d.ts.map +1 -1
- package/dist/discovery/openai.d.ts.map +1 -1
- package/dist/discovery/openrouter.d.ts +29 -5
- package/dist/discovery/openrouter.d.ts.map +1 -1
- package/dist/discovery/openrouter.js +56 -18
- package/dist/discovery/openrouter.js.map +1 -1
- package/dist/discovery/perplexity.d.ts.map +1 -1
- package/dist/discovery/promo-overrides.js.map +1 -1
- package/dist/discovery/static-direct.d.ts +9 -1
- package/dist/discovery/static-direct.d.ts.map +1 -1
- package/dist/discovery/static-direct.js +68 -7
- package/dist/discovery/static-direct.js.map +1 -1
- package/dist/discovery/together.d.ts.map +1 -1
- package/dist/discovery/vercel.d.ts +45 -0
- package/dist/discovery/vercel.d.ts.map +1 -0
- package/dist/discovery/vercel.js +232 -0
- package/dist/discovery/vercel.js.map +1 -0
- package/dist/discovery/vertex.d.ts.map +1 -1
- package/dist/discovery/vllm.d.ts +26 -0
- package/dist/discovery/vllm.d.ts.map +1 -0
- package/dist/discovery/vllm.js +93 -0
- package/dist/discovery/vllm.js.map +1 -0
- package/dist/discovery/zai.d.ts.map +1 -1
- package/dist/discovery-contract.d.ts +8 -1
- package/dist/discovery-contract.d.ts.map +1 -1
- package/dist/discovery-contract.js +28 -1
- package/dist/discovery-contract.js.map +1 -1
- package/dist/discovery-routes.d.ts +9 -0
- package/dist/discovery-routes.d.ts.map +1 -1
- package/dist/discovery-routes.js +106 -18
- package/dist/discovery-routes.js.map +1 -1
- package/dist/enrichment/litellm-catalog.d.ts +1 -0
- package/dist/enrichment/litellm-catalog.d.ts.map +1 -1
- package/dist/enrichment/litellm-catalog.js.map +1 -1
- package/dist/enrichment/litellm.d.ts +1 -1
- package/dist/enrichment/litellm.d.ts.map +1 -1
- package/dist/enrichment/litellm.js +9 -3
- package/dist/enrichment/litellm.js.map +1 -1
- package/dist/entry.d.ts +14 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +26 -0
- package/dist/entry.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.d.ts +207 -2
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +254 -54
- package/dist/mcp-server.js.map +1 -1
- package/dist/model-features.d.ts.map +1 -1
- package/dist/model-features.js +11 -2
- package/dist/model-features.js.map +1 -1
- package/dist/normalize.d.ts +7 -7
- package/dist/normalize.js +8 -8
- package/dist/provider-catalog.d.ts +17 -2
- package/dist/provider-catalog.d.ts.map +1 -1
- package/dist/provider-catalog.js +112 -0
- package/dist/provider-catalog.js.map +1 -1
- package/dist/proxy.d.ts +49 -7
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +765 -80
- package/dist/proxy.js.map +1 -1
- package/dist/registry-discovery.d.ts.map +1 -1
- package/dist/registry-discovery.js +4 -4
- package/dist/registry-discovery.js.map +1 -1
- package/dist/registry-query.d.ts +2 -2
- package/dist/registry-query.d.ts.map +1 -1
- package/dist/registry-query.js +60 -9
- package/dist/registry-query.js.map +1 -1
- package/dist/registry-routing.d.ts +65 -0
- package/dist/registry-routing.d.ts.map +1 -0
- package/dist/registry-routing.js +165 -0
- package/dist/registry-routing.js.map +1 -0
- package/dist/registry-runtime.d.ts +2 -0
- package/dist/registry-runtime.d.ts.map +1 -1
- package/dist/registry-runtime.js +167 -15
- package/dist/registry-runtime.js.map +1 -1
- package/dist/registry-selection.d.ts.map +1 -1
- package/dist/registry-selection.js +27 -3
- package/dist/registry-selection.js.map +1 -1
- package/dist/registry.d.ts +69 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +135 -3
- package/dist/registry.js.map +1 -1
- package/dist/resilience.d.ts +15 -0
- package/dist/resilience.d.ts.map +1 -1
- package/dist/resilience.js +23 -1
- package/dist/resilience.js.map +1 -1
- package/dist/security.js +6 -6
- package/dist/security.js.map +1 -1
- package/dist/server.d.ts +44 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +329 -34
- package/dist/server.js.map +1 -1
- package/dist/tally.d.ts +80 -0
- package/dist/tally.d.ts.map +1 -0
- package/dist/tally.js +176 -0
- package/dist/tally.js.map +1 -0
- package/dist/types.d.ts +64 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/wire-anthropic.d.ts +264 -0
- package/dist/wire-anthropic.d.ts.map +1 -0
- package/dist/wire-anthropic.js +960 -0
- package/dist/wire-anthropic.js.map +1 -0
- package/package.json +106 -97
- package/logo.png +0 -0
package/dist/cost.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kosha-discovery — Cost primitives shared by the proxy and the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Three pieces, all pure synthesis over the model + provider catalog:
|
|
5
|
+
* - Pre-flight cost estimation (input + expected-output tokens × rates).
|
|
6
|
+
* - JSONL spend ledger persisted under `~/.kosha/`, partitioned one file
|
|
7
|
+
* per calendar month (`ledger-YYYY-MM.jsonl`) so the budget gate only
|
|
8
|
+
* ever scans the month it cares about — not the full history. The legacy
|
|
9
|
+
* append-only `ledger.jsonl` is still read for backward compat so
|
|
10
|
+
* existing installs keep working until it stops growing.
|
|
11
|
+
* - Monthly budget gate driven by `KOSHA_MONTHLY_BUDGET_USD`.
|
|
12
|
+
*
|
|
13
|
+
* No tokenizer dependency — request bodies travel through the proxy
|
|
14
|
+
* untouched, so we approximate input tokens with a coarse char→token ratio.
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { appendFile, mkdir, readFile, readdir, rename, unlink, writeFile } from "fs/promises";
|
|
18
|
+
import { randomBytes } from "crypto";
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
import { dirname, join } from "path";
|
|
21
|
+
import { normalizeTokenUsage } from "./tally.js";
|
|
22
|
+
/**
|
|
23
|
+
* The USD figure a ledger row contributes to spend: an adjustment row's
|
|
24
|
+
* delta; otherwise the reconciled upstream cost when the row has one, the
|
|
25
|
+
* pre-flight estimate if not. Every reader (budget gate, `kosha spend`,
|
|
26
|
+
* /metrics) goes through this so they agree.
|
|
27
|
+
*/
|
|
28
|
+
export function ledgerRowUsd(row) {
|
|
29
|
+
if (row.kind === "adjustment") {
|
|
30
|
+
return typeof row.adjustmentUsd === "number" && Number.isFinite(row.adjustmentUsd) ? row.adjustmentUsd : 0;
|
|
31
|
+
}
|
|
32
|
+
if (typeof row.actualUsd === "number" && Number.isFinite(row.actualUsd))
|
|
33
|
+
return row.actualUsd;
|
|
34
|
+
return typeof row.estimatedUsd === "number" && Number.isFinite(row.estimatedUsd) ? row.estimatedUsd : 0;
|
|
35
|
+
}
|
|
36
|
+
/** True for rows that represent a completed request (not a later correction). */
|
|
37
|
+
export function isRequestRow(row) {
|
|
38
|
+
return row.kind !== "adjustment";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Turn a provider `usage` block into a reconciled USD cost against the
|
|
42
|
+
* model's pricing. Returns null when the model has no usable pricing or the
|
|
43
|
+
* block carries no token counts. Cache rates fall back to the provider's
|
|
44
|
+
* published ratios (Anthropic: reads 0.1×, writes 1.25× input; OpenAI: reads
|
|
45
|
+
* 0.5× input) when the catalog entry lacks explicit cache pricing, so a
|
|
46
|
+
* cached request is never priced as if it were fully uncached.
|
|
47
|
+
*/
|
|
48
|
+
export function actualCostFromUsage(model, rawUsage, shape) {
|
|
49
|
+
const pricing = model.pricing;
|
|
50
|
+
if (!pricing || typeof pricing.inputPerMillion !== "number" || typeof pricing.outputPerMillion !== "number")
|
|
51
|
+
return null;
|
|
52
|
+
if (!rawUsage || typeof rawUsage !== "object")
|
|
53
|
+
return null;
|
|
54
|
+
const usage = normalizeTokenUsage(rawUsage);
|
|
55
|
+
if (!usage)
|
|
56
|
+
return null;
|
|
57
|
+
const cacheRead = usage.cachedInputTokens ?? 0;
|
|
58
|
+
const cacheWrite = usage.cacheWriteTokens ?? 0;
|
|
59
|
+
const uncachedInput = shape === "openai" ? Math.max(0, usage.inputTokens - cacheRead) : usage.inputTokens;
|
|
60
|
+
const readRate = pricing.cacheReadPerMillion ?? pricing.inputPerMillion * (shape === "anthropic" ? 0.1 : 0.5);
|
|
61
|
+
const writeRate = pricing.cacheWritePerMillion ?? (shape === "anthropic" ? pricing.inputPerMillion * 1.25 : 0);
|
|
62
|
+
const usd = tokensTimesRate(uncachedInput, pricing.inputPerMillion) +
|
|
63
|
+
tokensTimesRate(usage.outputTokens, pricing.outputPerMillion) +
|
|
64
|
+
tokensTimesRate(cacheRead, readRate) +
|
|
65
|
+
tokensTimesRate(cacheWrite, writeRate);
|
|
66
|
+
if (!Number.isFinite(usd) || usd < 0)
|
|
67
|
+
return null;
|
|
68
|
+
return { usd, inputTokens: uncachedInput, outputTokens: usage.outputTokens, cacheReadTokens: cacheRead, cacheWriteTokens: cacheWrite };
|
|
69
|
+
}
|
|
70
|
+
export const DEFAULT_LEDGER_PATH = join(homedir(), ".kosha", "ledger.jsonl");
|
|
71
|
+
/**
|
|
72
|
+
* How many monthly partitions to retain on disk. Older partitions are
|
|
73
|
+
* trimmed opportunistically on append. Tunable via
|
|
74
|
+
* `KOSHA_LEDGER_RETENTION_MONTHS`; non-positive / unparseable values fall
|
|
75
|
+
* back to the default. The legacy `ledger.jsonl` is never trimmed by the
|
|
76
|
+
* partition sweeper — only `ledger-YYYY-MM.jsonl` siblings are.
|
|
77
|
+
*/
|
|
78
|
+
const DEFAULT_RETENTION_MONTHS = 12;
|
|
79
|
+
/**
|
|
80
|
+
* Conservative chars-per-token used when we don't have a tokenizer wired up.
|
|
81
|
+
* This is intentionally lower than the OpenAI rule of thumb (~4) so the
|
|
82
|
+
* estimate skews high; underestimating spend is the dangerous direction.
|
|
83
|
+
*/
|
|
84
|
+
const CHARS_PER_TOKEN = 3.5;
|
|
85
|
+
/** Output tokens to assume when the caller doesn't request `max_tokens`. */
|
|
86
|
+
const DEFAULT_EXPECTED_OUTPUT_TOKENS = 512;
|
|
87
|
+
/** Per-MTok rates are stored in USD; price = (tokens/1_000_000) * rate. */
|
|
88
|
+
function tokensTimesRate(tokens, perMillion) {
|
|
89
|
+
return (tokens / 1_000_000) * perMillion;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Estimate the USD cost of forwarding `requestBody` to `model`. Returns null
|
|
93
|
+
* when the model has no usable pricing — the caller should treat that as
|
|
94
|
+
* "cost unknown" rather than free.
|
|
95
|
+
*/
|
|
96
|
+
export function estimateRequestCost(model, requestBody) {
|
|
97
|
+
const pricing = model.pricing;
|
|
98
|
+
if (!pricing)
|
|
99
|
+
return null;
|
|
100
|
+
if (typeof pricing.inputPerMillion !== "number" || typeof pricing.outputPerMillion !== "number")
|
|
101
|
+
return null;
|
|
102
|
+
const inputTokens = approximateInputTokens(requestBody);
|
|
103
|
+
// `max_completion_tokens` is OpenAI's current spelling; `max_tokens` the legacy one.
|
|
104
|
+
const requestedOutput = typeof requestBody.max_completion_tokens === "number" && requestBody.max_completion_tokens > 0
|
|
105
|
+
? requestBody.max_completion_tokens
|
|
106
|
+
: typeof requestBody.max_tokens === "number" && requestBody.max_tokens > 0
|
|
107
|
+
? requestBody.max_tokens
|
|
108
|
+
: undefined;
|
|
109
|
+
const expectedOutputTokens = requestedOutput !== undefined ? Math.floor(requestedOutput) : DEFAULT_EXPECTED_OUTPUT_TOKENS;
|
|
110
|
+
const estimatedUsd = tokensTimesRate(inputTokens, pricing.inputPerMillion) +
|
|
111
|
+
tokensTimesRate(expectedOutputTokens, pricing.outputPerMillion);
|
|
112
|
+
return { estimatedUsd, inputTokens, expectedOutputTokens };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Approximate input token count by walking the OpenAI-style `messages`
|
|
116
|
+
* array, falling back to total stringified body length. Intentionally
|
|
117
|
+
* conservative (small CHARS_PER_TOKEN) so the estimate doesn't undershoot.
|
|
118
|
+
*/
|
|
119
|
+
function approximateInputTokens(body) {
|
|
120
|
+
const messages = body.messages;
|
|
121
|
+
if (Array.isArray(messages)) {
|
|
122
|
+
let chars = 0;
|
|
123
|
+
for (const msg of messages) {
|
|
124
|
+
if (!msg || typeof msg !== "object")
|
|
125
|
+
continue;
|
|
126
|
+
const content = msg.content;
|
|
127
|
+
if (typeof content === "string") {
|
|
128
|
+
chars += content.length;
|
|
129
|
+
}
|
|
130
|
+
else if (Array.isArray(content)) {
|
|
131
|
+
for (const part of content) {
|
|
132
|
+
if (typeof part === "string")
|
|
133
|
+
chars += part.length;
|
|
134
|
+
else if (part && typeof part === "object" && typeof part.text === "string") {
|
|
135
|
+
chars += part.text.length;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (chars > 0)
|
|
141
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
142
|
+
}
|
|
143
|
+
const fallback = JSON.stringify(body).length;
|
|
144
|
+
return Math.ceil(fallback / CHARS_PER_TOKEN);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read `KOSHA_MONTHLY_BUDGET_USD` (if any). Returns null when no budget is
|
|
148
|
+
* configured — the proxy's budget gate then becomes a no-op. This cap is
|
|
149
|
+
* global: it is always compared against total spend, never a tenant's slice,
|
|
150
|
+
* so a caller cannot escape it by inventing a fresh tenant tag.
|
|
151
|
+
*/
|
|
152
|
+
export function readMonthlyBudgetUsd() {
|
|
153
|
+
return readPositiveUsd(process.env.KOSHA_MONTHLY_BUDGET_USD);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Read `KOSHA_TENANT_BUDGET_USD` (if any): an additional per-tenant monthly
|
|
157
|
+
* cap applied on top of the global one to requests that carry a tenant tag.
|
|
158
|
+
*/
|
|
159
|
+
export function readTenantBudgetUsd() {
|
|
160
|
+
return readPositiveUsd(process.env.KOSHA_TENANT_BUDGET_USD);
|
|
161
|
+
}
|
|
162
|
+
function readPositiveUsd(raw) {
|
|
163
|
+
if (!raw)
|
|
164
|
+
return null;
|
|
165
|
+
const parsed = Number.parseFloat(raw);
|
|
166
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Sum estimated USD spend for entries falling in the calendar month that
|
|
170
|
+
* contains `nowMs`. Tenant scopes the sum when given.
|
|
171
|
+
*
|
|
172
|
+
* Reads only the month's partition (`ledger-YYYY-MM.jsonl` next to
|
|
173
|
+
* `ledgerPath`) plus, for backward compat, the legacy append-only
|
|
174
|
+
* `ledgerPath` itself — so existing installs whose history still lives in
|
|
175
|
+
* the un-partitioned file keep counting until that file stops growing. At
|
|
176
|
+
* most two files are read, never the full history.
|
|
177
|
+
*/
|
|
178
|
+
export async function readSpendForMonth(nowMs, tenant, ledgerPath = DEFAULT_LEDGER_PATH) {
|
|
179
|
+
const cutoffStart = startOfMonth(nowMs);
|
|
180
|
+
const cutoffEnd = startOfNextMonth(nowMs);
|
|
181
|
+
// The month partition is the hot path; the legacy file is the fallback
|
|
182
|
+
// for pre-partitioning history. Order matters only for readability —
|
|
183
|
+
// both are filtered by the same ts window below.
|
|
184
|
+
const sources = [partitionPathFor(ledgerPath, nowMs), ledgerPath];
|
|
185
|
+
let total = 0;
|
|
186
|
+
for (const file of sources) {
|
|
187
|
+
let raw;
|
|
188
|
+
try {
|
|
189
|
+
raw = await readFile(file, "utf-8");
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
// Missing file (partition not yet created, or legacy absent) is
|
|
193
|
+
// a clean "nothing here" — try the next source.
|
|
194
|
+
if (err?.code === "ENOENT")
|
|
195
|
+
continue;
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
for (const line of raw.split("\n")) {
|
|
199
|
+
if (!line)
|
|
200
|
+
continue;
|
|
201
|
+
let row;
|
|
202
|
+
try {
|
|
203
|
+
row = JSON.parse(line);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
continue; // skip corrupt lines, don't crash budget enforcement
|
|
207
|
+
}
|
|
208
|
+
if (row.ts < cutoffStart || row.ts >= cutoffEnd)
|
|
209
|
+
continue;
|
|
210
|
+
if (tenant && row.tenant !== tenant)
|
|
211
|
+
continue;
|
|
212
|
+
total += ledgerRowUsd(row);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return total;
|
|
216
|
+
}
|
|
217
|
+
/** Strip control characters and bound length on a ledger string field. The
|
|
218
|
+
* ledger records caller-supplied values (modelId, tenant tag, the original
|
|
219
|
+
* requested model string), so we sanitize them before writing to disk: each
|
|
220
|
+
* line stays a single, well-formed JSON record. Without this, a crafted
|
|
221
|
+
* model id with embedded CR/LF could split one row into two. */
|
|
222
|
+
function sanitizeLedgerString(value) {
|
|
223
|
+
return value.replace(/[\r\n\t]/g, "").slice(0, 256);
|
|
224
|
+
}
|
|
225
|
+
/** Apply sanitizeLedgerString to every string field of the entry. Numeric
|
|
226
|
+
* fields are pass-through. */
|
|
227
|
+
function sanitizeEntry(entry) {
|
|
228
|
+
return {
|
|
229
|
+
ts: entry.ts,
|
|
230
|
+
provider: sanitizeLedgerString(entry.provider),
|
|
231
|
+
modelId: sanitizeLedgerString(entry.modelId),
|
|
232
|
+
requested: sanitizeLedgerString(entry.requested),
|
|
233
|
+
tenant: entry.tenant === null ? null : sanitizeLedgerString(entry.tenant),
|
|
234
|
+
estimatedUsd: entry.estimatedUsd,
|
|
235
|
+
estimatedInputTokens: entry.estimatedInputTokens,
|
|
236
|
+
estimatedOutputTokens: entry.estimatedOutputTokens,
|
|
237
|
+
upstreamStatus: entry.upstreamStatus,
|
|
238
|
+
...(entry.actualUsd !== undefined ? { actualUsd: entry.actualUsd } : {}),
|
|
239
|
+
...(entry.actualInputTokens !== undefined ? { actualInputTokens: entry.actualInputTokens } : {}),
|
|
240
|
+
...(entry.actualOutputTokens !== undefined ? { actualOutputTokens: entry.actualOutputTokens } : {}),
|
|
241
|
+
...(entry.cacheReadTokens !== undefined ? { cacheReadTokens: entry.cacheReadTokens } : {}),
|
|
242
|
+
...(entry.cacheWriteTokens !== undefined ? { cacheWriteTokens: entry.cacheWriteTokens } : {}),
|
|
243
|
+
...(entry.usageSource !== undefined ? { usageSource: entry.usageSource } : {}),
|
|
244
|
+
...(entry.kind !== undefined ? { kind: entry.kind } : {}),
|
|
245
|
+
...(entry.requestId !== undefined ? { requestId: sanitizeLedgerString(entry.requestId) } : {}),
|
|
246
|
+
...(entry.adjustmentUsd !== undefined ? { adjustmentUsd: entry.adjustmentUsd } : {}),
|
|
247
|
+
...(entry.adjustmentInputTokens !== undefined ? { adjustmentInputTokens: entry.adjustmentInputTokens } : {}),
|
|
248
|
+
...(entry.adjustmentOutputTokens !== undefined ? { adjustmentOutputTokens: entry.adjustmentOutputTokens } : {}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/** `YYYY-MM` (zero-padded month) for the UTC calendar month containing ts.
|
|
252
|
+
* UTC matches startOfMonth/startOfNextMonth so the partition a row lands in
|
|
253
|
+
* is exactly the month the budget cutoffs bracket. */
|
|
254
|
+
function monthKeyFromTs(ts) {
|
|
255
|
+
const d = new Date(ts);
|
|
256
|
+
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
257
|
+
}
|
|
258
|
+
/** Parse a `YYYY-MM` key back to its UTC month-start epoch ms, or null when
|
|
259
|
+
* the shape is wrong. Used by trim to age partitions out deterministically. */
|
|
260
|
+
function monthStartFromKey(key) {
|
|
261
|
+
const m = /^(\d{4})-(\d{2})$/.exec(key);
|
|
262
|
+
if (!m)
|
|
263
|
+
return null;
|
|
264
|
+
const year = Number(m[1]);
|
|
265
|
+
const month = Number(m[2]) - 1; // JS months are 0-indexed
|
|
266
|
+
if (month < 0 || month > 11)
|
|
267
|
+
return null;
|
|
268
|
+
return Date.UTC(year, month, 1);
|
|
269
|
+
}
|
|
270
|
+
/** Path of the monthly partition file a given ts belongs in. Partitions are
|
|
271
|
+
* siblings of the legacy ledger file so they share its directory and perms. */
|
|
272
|
+
function partitionPathFor(ledgerPath, ts) {
|
|
273
|
+
return join(dirname(ledgerPath), `ledger-${monthKeyFromTs(ts)}.jsonl`);
|
|
274
|
+
}
|
|
275
|
+
/** Read the retention window from `KOSHA_LEDGER_RETENTION_MONTHS`, falling
|
|
276
|
+
* back to the default for missing/non-positive/unparseable values. */
|
|
277
|
+
function readRetentionMonths() {
|
|
278
|
+
const raw = process.env.KOSHA_LEDGER_RETENTION_MONTHS;
|
|
279
|
+
if (!raw)
|
|
280
|
+
return DEFAULT_RETENTION_MONTHS;
|
|
281
|
+
const parsed = Number.parseInt(raw, 10);
|
|
282
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_RETENTION_MONTHS;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Drop monthly partition files older than the retention window. Best-effort:
|
|
286
|
+
* errors are swallowed because retention is housekeeping, not correctness.
|
|
287
|
+
* The legacy `ledgerPath` file itself is never touched — only
|
|
288
|
+
* `ledger-YYYY-MM.jsonl` siblings are eligible. Returns the count of
|
|
289
|
+
* partitions removed (handy for tests / a future `kosha ledger trim`).
|
|
290
|
+
*
|
|
291
|
+
* The window is "current month plus `retentionMonths - 1` prior months", so
|
|
292
|
+
* the default of 12 keeps a rolling year. Exported so the CLI can run an
|
|
293
|
+
* explicit sweep without waiting for an append.
|
|
294
|
+
*/
|
|
295
|
+
export async function trimLedgerPartitions(ledgerPath, nowMs, retentionMonths = readRetentionMonths()) {
|
|
296
|
+
const dir = dirname(ledgerPath);
|
|
297
|
+
const d = new Date(nowMs);
|
|
298
|
+
// First month we keep: current month shifted back (N-1). Date.UTC wraps
|
|
299
|
+
// negative months into prior years correctly (month -5 of 2026 → Aug 2025).
|
|
300
|
+
const cutoffStart = Date.UTC(d.getUTCFullYear(), d.getUTCMonth() - (retentionMonths - 1), 1);
|
|
301
|
+
let entries;
|
|
302
|
+
try {
|
|
303
|
+
entries = await readdir(dir);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// No directory yet (ENOENT) or unreadable — nothing to trim.
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
let removed = 0;
|
|
310
|
+
for (const name of entries) {
|
|
311
|
+
const m = /^ledger-(\d{4})-(\d{2})\.jsonl$/.exec(name);
|
|
312
|
+
if (!m)
|
|
313
|
+
continue;
|
|
314
|
+
const start = monthStartFromKey(`${m[1]}-${m[2]}`);
|
|
315
|
+
if (start === null || start >= cutoffStart)
|
|
316
|
+
continue;
|
|
317
|
+
await unlink(join(dir, name)).catch(() => {
|
|
318
|
+
// raced with another sweeper or permission issue — fine, best-effort
|
|
319
|
+
});
|
|
320
|
+
removed += 1;
|
|
321
|
+
}
|
|
322
|
+
return removed;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Append one ledger entry atomically. The entry is written to the monthly
|
|
326
|
+
* partition for its own `ts` (`ledger-YYYY-MM.jsonl`), keeping each file
|
|
327
|
+
* bounded to a single month so budget reads don't scan the full history.
|
|
328
|
+
*
|
|
329
|
+
* Writes are append-only so concurrent processes don't clobber each other.
|
|
330
|
+
* A best-effort atomic create-and-rename is used if the first append hits
|
|
331
|
+
* ENOENT, so a fresh install comes up cleanly. After the write we run a
|
|
332
|
+
* best-effort retention trim so old partitions age out without a cron job.
|
|
333
|
+
*/
|
|
334
|
+
export async function appendLedgerEntry(entry, ledgerPath = DEFAULT_LEDGER_PATH) {
|
|
335
|
+
await mkdir(dirname(ledgerPath), { recursive: true });
|
|
336
|
+
const partPath = partitionPathFor(ledgerPath, entry.ts);
|
|
337
|
+
const line = `${JSON.stringify(sanitizeEntry(entry))}\n`;
|
|
338
|
+
try {
|
|
339
|
+
await appendFile(partPath, line, "utf-8");
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
// If append fails (e.g. permissions) we just swallow — ledger is
|
|
343
|
+
// observability, not load-bearing routing state. A best-effort
|
|
344
|
+
// create-and-rename gets us going on a fresh install.
|
|
345
|
+
if (err?.code === "ENOENT") {
|
|
346
|
+
const tmp = `${partPath}.${randomBytes(4).toString("hex")}.tmp`;
|
|
347
|
+
try {
|
|
348
|
+
await writeFile(tmp, line, "utf-8");
|
|
349
|
+
await rename(tmp, partPath);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// give up silently
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Opportunistic, best-effort retention sweep. Housekeeping must never
|
|
357
|
+
// break the append — the ledger is observability.
|
|
358
|
+
try {
|
|
359
|
+
await trimLedgerPartitions(ledgerPath, entry.ts);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// swallowed
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function startOfMonth(nowMs) {
|
|
366
|
+
const d = new Date(nowMs);
|
|
367
|
+
// Important: pair `getUTCMonth()`/`getUTCFullYear()` with `Date.UTC()`.
|
|
368
|
+
// The numeric `new Date(year, month, …)` constructor interprets its args
|
|
369
|
+
// as LOCAL time, so the previous code drifted by up to ~24h around month
|
|
370
|
+
// boundaries in any non-UTC timezone — breaking budget cutoffs.
|
|
371
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
|
|
372
|
+
}
|
|
373
|
+
function startOfNextMonth(nowMs) {
|
|
374
|
+
const d = new Date(nowMs);
|
|
375
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1);
|
|
376
|
+
}
|
|
377
|
+
//# sourceMappingURL=cost.js.map
|
package/dist/cost.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost.js","sourceRoot":"","sources":["../src/cost.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,6 +29,16 @@ import type { CredentialResult } from "../types.js";
|
|
|
29
29
|
export declare class CredentialResolver {
|
|
30
30
|
private readonly home;
|
|
31
31
|
private readonly isWindows;
|
|
32
|
+
/**
|
|
33
|
+
* Return every plausible config-directory path for a per-tool config file,
|
|
34
|
+
* in resolution order. We honour both common Linux conventions
|
|
35
|
+
* (`$XDG_CONFIG_HOME/<tool>`, `~/.config/<tool>`) and the Windows
|
|
36
|
+
* convention (`%APPDATA%\<tool>`), so a user with credentials in any
|
|
37
|
+
* standard location is found rather than silently skipped.
|
|
38
|
+
*/
|
|
39
|
+
private toolConfigCandidates;
|
|
40
|
+
/** Read the first existing JSON file from a list of candidate paths. */
|
|
41
|
+
private readFirstJson;
|
|
32
42
|
/**
|
|
33
43
|
* Resolve credentials for the given provider.
|
|
34
44
|
*
|
|
@@ -71,6 +81,13 @@ export declare class CredentialResolver {
|
|
|
71
81
|
* (No known CLI tool stores OpenRouter keys on disk.)
|
|
72
82
|
*/
|
|
73
83
|
private resolveOpenRouter;
|
|
84
|
+
/**
|
|
85
|
+
* Search hierarchy for Vercel AI Gateway credentials:
|
|
86
|
+
* 1. Explicit config key
|
|
87
|
+
* 2. `AI_GATEWAY_API_KEY` env var
|
|
88
|
+
* 3. `VERCEL_OIDC_TOKEN` env var (available on Vercel deployments)
|
|
89
|
+
*/
|
|
90
|
+
private resolveVercelAIGateway;
|
|
74
91
|
/**
|
|
75
92
|
* Ollama runs locally and never requires authentication.
|
|
76
93
|
* Returns `{ source: "none" }` unconditionally.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/credentials/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD;;;;;;;;;;GAUG;AACH,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IAEpD;;;;;;;OAOG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/credentials/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD;;;;;;;;;;GAUG;AACH,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IAEpD;;;;;;OAMG;IACH,OAAO,CAAC,oBAAoB;IAU5B,wEAAwE;YAC1D,aAAa;IAQ3B;;;;;;;OAOG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmDjF;IAMD;;;;;;;;OAQG;YACW,gBAAgB;IAuD9B;;;;;;OAMG;YACW,aAAa;IA2C3B;;;;;;OAMG;YACW,aAAa;IA4C3B;;;;;OAKG;YACW,iBAAiB;IAoB/B;;;;;OAKG;YACW,sBAAsB;IAsBpC;;;OAGG;IACH,OAAO,CAAC,aAAa;IAQrB;;;;;;;;;;;;;OAaG;YACW,wBAAwB;IAqDtC;;;;;;;;;OASG;YACW,gBAAgB;IAsB9B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,kBAAkB;IA2C1B;;;;;;;;;;;;;;OAcG;YACW,uBAAuB;IA0CrC;;;;;;;OAOG;YACW,qBAAqB;IAkBnC;;;OAGG;IACH,OAAO,CAAC,eAAe;IAavB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAkBzB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,kBAAkB;IAmB1B,oEAAoE;YACtD,QAAQ;IAStB,sEAAsE;YACxD,YAAY;IAQ1B,8DAA8D;IAC9D,OAAO,CAAC,eAAe;IAiBvB,6CAA6C;IAC7C,OAAO,CAAC,cAAc;IAetB,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB;CAS3B"}
|
|
@@ -33,6 +33,33 @@ import { normalizeProviderId } from "../provider-catalog.js";
|
|
|
33
33
|
export class CredentialResolver {
|
|
34
34
|
home = homedir();
|
|
35
35
|
isWindows = platform() === "win32";
|
|
36
|
+
/**
|
|
37
|
+
* Return every plausible config-directory path for a per-tool config file,
|
|
38
|
+
* in resolution order. We honour both common Linux conventions
|
|
39
|
+
* (`$XDG_CONFIG_HOME/<tool>`, `~/.config/<tool>`) and the Windows
|
|
40
|
+
* convention (`%APPDATA%\<tool>`), so a user with credentials in any
|
|
41
|
+
* standard location is found rather than silently skipped.
|
|
42
|
+
*/
|
|
43
|
+
toolConfigCandidates(tool, file) {
|
|
44
|
+
const paths = [];
|
|
45
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
46
|
+
if (xdg)
|
|
47
|
+
paths.push(join(xdg, tool, file));
|
|
48
|
+
paths.push(join(this.home, ".config", tool, file));
|
|
49
|
+
const appData = process.env.APPDATA;
|
|
50
|
+
if (this.isWindows && appData)
|
|
51
|
+
paths.push(join(appData, tool, file));
|
|
52
|
+
return paths;
|
|
53
|
+
}
|
|
54
|
+
/** Read the first existing JSON file from a list of candidate paths. */
|
|
55
|
+
async readFirstJson(paths) {
|
|
56
|
+
for (const path of paths) {
|
|
57
|
+
const data = await this.readJson(path);
|
|
58
|
+
if (data !== null)
|
|
59
|
+
return { data, path };
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
36
63
|
/**
|
|
37
64
|
* Resolve credentials for the given provider.
|
|
38
65
|
*
|
|
@@ -52,6 +79,8 @@ export class CredentialResolver {
|
|
|
52
79
|
return this.resolveGoogle(explicitKey);
|
|
53
80
|
case "openrouter":
|
|
54
81
|
return this.resolveOpenRouter(explicitKey);
|
|
82
|
+
case "vercel":
|
|
83
|
+
return this.resolveVercelAIGateway(explicitKey);
|
|
55
84
|
case "ollama":
|
|
56
85
|
case "llama.cpp":
|
|
57
86
|
return this.resolveOllama();
|
|
@@ -113,19 +142,19 @@ export class CredentialResolver {
|
|
|
113
142
|
if (envKey) {
|
|
114
143
|
return { apiKey: envKey, source: "env" };
|
|
115
144
|
}
|
|
116
|
-
// 3. Claude CLI config — ~/.claude.json
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
145
|
+
// 3. Claude CLI config — ~/.claude.json (also %APPDATA%\claude\config.json on Windows)
|
|
146
|
+
const claudeJsonPaths = [join(this.home, ".claude.json")];
|
|
147
|
+
const appData = process.env.APPDATA;
|
|
148
|
+
if (this.isWindows && appData)
|
|
149
|
+
claudeJsonPaths.push(join(appData, "claude", "config.json"));
|
|
150
|
+
const claudeJson = await this.readFirstJson(claudeJsonPaths);
|
|
151
|
+
if (claudeJson?.data?.apiKey) {
|
|
152
|
+
return { apiKey: claudeJson.data.apiKey, source: "cli", path: claudeJson.path };
|
|
120
153
|
}
|
|
121
|
-
// 4. Claude CLI settings — ~/.config/
|
|
122
|
-
const claudeSettings = await this.
|
|
123
|
-
if (claudeSettings?.apiKey) {
|
|
124
|
-
return {
|
|
125
|
-
apiKey: claudeSettings.apiKey,
|
|
126
|
-
source: "cli",
|
|
127
|
-
path: join(this.home, ".config", "claude", "settings.json"),
|
|
128
|
-
};
|
|
154
|
+
// 4. Claude CLI settings — XDG / ~/.config / APPDATA.
|
|
155
|
+
const claudeSettings = await this.readFirstJson(this.toolConfigCandidates("claude", "settings.json"));
|
|
156
|
+
if (claudeSettings?.data?.apiKey) {
|
|
157
|
+
return { apiKey: claudeSettings.data.apiKey, source: "cli", path: claudeSettings.path };
|
|
129
158
|
}
|
|
130
159
|
// 5. Claude CLI OAuth — ~/.claude/credentials.json
|
|
131
160
|
const claudeOAuth = await this.readJson(join(this.home, ".claude", "credentials.json"));
|
|
@@ -136,10 +165,11 @@ export class CredentialResolver {
|
|
|
136
165
|
path: join(this.home, ".claude", "credentials.json"),
|
|
137
166
|
};
|
|
138
167
|
}
|
|
139
|
-
// 6. Codex CLI — ~/.codex/auth.json
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
168
|
+
// 6. Codex CLI — ~/.codex/auth.json, also XDG/APPDATA fallbacks.
|
|
169
|
+
const codexPaths = [join(this.home, ".codex", "auth.json"), ...this.toolConfigCandidates("codex", "auth.json")];
|
|
170
|
+
const codexAuth = await this.readFirstJson(codexPaths);
|
|
171
|
+
if (codexAuth?.data?.anthropic) {
|
|
172
|
+
return { apiKey: codexAuth.data.anthropic, source: "cli", path: codexAuth.path };
|
|
143
173
|
}
|
|
144
174
|
return { source: "none" };
|
|
145
175
|
}
|
|
@@ -207,21 +237,17 @@ export class CredentialResolver {
|
|
|
207
237
|
if (envKey) {
|
|
208
238
|
return { apiKey: envKey, source: "env" };
|
|
209
239
|
}
|
|
210
|
-
// 3. Gemini CLI credentials
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
};
|
|
240
|
+
// 3. Gemini CLI credentials — ~/.gemini, also XDG/APPDATA fallbacks.
|
|
241
|
+
const geminiPaths = [
|
|
242
|
+
join(this.home, ".gemini", "credentials.json"),
|
|
243
|
+
...this.toolConfigCandidates("gemini", "credentials.json"),
|
|
244
|
+
];
|
|
245
|
+
const geminiCreds = await this.readFirstJson(geminiPaths);
|
|
246
|
+
if (geminiCreds?.data?.apiKey) {
|
|
247
|
+
return { apiKey: geminiCreds.data.apiKey, source: "cli", path: geminiCreds.path };
|
|
218
248
|
}
|
|
219
|
-
if (geminiCreds?.access_token) {
|
|
220
|
-
return {
|
|
221
|
-
accessToken: geminiCreds.access_token,
|
|
222
|
-
source: "cli",
|
|
223
|
-
path: join(this.home, ".gemini", "credentials.json"),
|
|
224
|
-
};
|
|
249
|
+
if (geminiCreds?.data?.access_token) {
|
|
250
|
+
return { accessToken: geminiCreds.data.access_token, source: "cli", path: geminiCreds.path };
|
|
225
251
|
}
|
|
226
252
|
// 4. gcloud application default credentials
|
|
227
253
|
const gcloudPaths = this.getGcloudPaths();
|
|
@@ -259,6 +285,29 @@ export class CredentialResolver {
|
|
|
259
285
|
return { source: "none" };
|
|
260
286
|
}
|
|
261
287
|
// ---------------------------------------------------------------------------
|
|
288
|
+
// Vercel AI Gateway
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
/**
|
|
291
|
+
* Search hierarchy for Vercel AI Gateway credentials:
|
|
292
|
+
* 1. Explicit config key
|
|
293
|
+
* 2. `AI_GATEWAY_API_KEY` env var
|
|
294
|
+
* 3. `VERCEL_OIDC_TOKEN` env var (available on Vercel deployments)
|
|
295
|
+
*/
|
|
296
|
+
async resolveVercelAIGateway(explicitKey) {
|
|
297
|
+
if (explicitKey) {
|
|
298
|
+
return { apiKey: explicitKey, source: "config" };
|
|
299
|
+
}
|
|
300
|
+
const apiKey = process.env.AI_GATEWAY_API_KEY;
|
|
301
|
+
if (apiKey) {
|
|
302
|
+
return { apiKey, source: "env" };
|
|
303
|
+
}
|
|
304
|
+
const oidcToken = process.env.VERCEL_OIDC_TOKEN;
|
|
305
|
+
if (oidcToken) {
|
|
306
|
+
return { accessToken: oidcToken, source: "env" };
|
|
307
|
+
}
|
|
308
|
+
return { source: "none" };
|
|
309
|
+
}
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
262
311
|
// Ollama (no auth needed)
|
|
263
312
|
// ---------------------------------------------------------------------------
|
|
264
313
|
/**
|
|
@@ -309,7 +358,7 @@ export class CredentialResolver {
|
|
|
309
358
|
const awsCredentialsRaw = await this.readTextFile(awsCredentialsPath);
|
|
310
359
|
if (awsCredentialsRaw !== null) {
|
|
311
360
|
const defaultProfile = this.parseAwsIniSection(awsCredentialsRaw, "default");
|
|
312
|
-
const keyId = defaultProfile
|
|
361
|
+
const keyId = defaultProfile.aws_access_key_id;
|
|
313
362
|
if (keyId) {
|
|
314
363
|
const region = await this.resolveAwsRegion();
|
|
315
364
|
return { apiKey: keyId, source: "cli", path: awsCredentialsPath, metadata: { region } };
|
|
@@ -352,8 +401,8 @@ export class CredentialResolver {
|
|
|
352
401
|
const configText = awsConfigRaw ?? (await this.readTextFile(join(this.home, ".aws", "config")));
|
|
353
402
|
if (configText !== null) {
|
|
354
403
|
// AWS config uses [profile default] for non-default profiles and [default] for the default
|
|
355
|
-
const section = this.parseAwsIniSection(configText, "profile default")
|
|
356
|
-
this.parseAwsIniSection(configText, "default")
|
|
404
|
+
const section = this.parseAwsIniSection(configText, "profile default").region ??
|
|
405
|
+
this.parseAwsIniSection(configText, "default").region;
|
|
357
406
|
if (section) {
|
|
358
407
|
return section;
|
|
359
408
|
}
|
|
@@ -379,7 +428,16 @@ export class CredentialResolver {
|
|
|
379
428
|
const trimmed = line.trim();
|
|
380
429
|
// Detect section headers
|
|
381
430
|
if (trimmed.startsWith("[")) {
|
|
382
|
-
|
|
431
|
+
// A line that opens a section but never closes it is malformed.
|
|
432
|
+
// `indexOf("]")` returns -1 there, and `slice(1, -1)` would
|
|
433
|
+
// silently extract "everything but the last char" — quietly
|
|
434
|
+
// matching the wrong section. Skip the line instead.
|
|
435
|
+
const end = trimmed.indexOf("]");
|
|
436
|
+
if (end === -1) {
|
|
437
|
+
inSection = false;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const header = trimmed.slice(1, end).trim();
|
|
383
441
|
inSection = header === sectionName;
|
|
384
442
|
continue;
|
|
385
443
|
}
|