@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.
Files changed (191) hide show
  1. package/README.md +66 -26
  2. package/dist/aliases.d.ts +6 -1
  3. package/dist/aliases.d.ts.map +1 -1
  4. package/dist/aliases.js +38 -12
  5. package/dist/aliases.js.map +1 -1
  6. package/dist/cache.d.ts.map +1 -1
  7. package/dist/cache.js +25 -2
  8. package/dist/cache.js.map +1 -1
  9. package/dist/claude-generation.d.ts +59 -0
  10. package/dist/claude-generation.d.ts.map +1 -0
  11. package/dist/claude-generation.js +117 -0
  12. package/dist/claude-generation.js.map +1 -0
  13. package/dist/cli-cmd-doctor.d.ts +17 -0
  14. package/dist/cli-cmd-doctor.d.ts.map +1 -0
  15. package/dist/cli-cmd-doctor.js +199 -0
  16. package/dist/cli-cmd-doctor.js.map +1 -0
  17. package/dist/cli-cmd-model.js +2 -2
  18. package/dist/cli-cmd-model.js.map +1 -1
  19. package/dist/cli-cmd-spend.d.ts +10 -0
  20. package/dist/cli-cmd-spend.d.ts.map +1 -0
  21. package/dist/cli-cmd-spend.js +130 -0
  22. package/dist/cli-cmd-spend.js.map +1 -0
  23. package/dist/cli-commands.d.ts +4 -1
  24. package/dist/cli-commands.d.ts.map +1 -1
  25. package/dist/cli-commands.js +10 -2
  26. package/dist/cli-commands.js.map +1 -1
  27. package/dist/cli-format.d.ts.map +1 -1
  28. package/dist/cli-format.js +2 -1
  29. package/dist/cli-format.js.map +1 -1
  30. package/dist/cli-help.d.ts.map +1 -1
  31. package/dist/cli-help.js +4 -2
  32. package/dist/cli-help.js.map +1 -1
  33. package/dist/cli.js +10 -0
  34. package/dist/cli.js.map +1 -1
  35. package/dist/context-strategy.d.ts +48 -0
  36. package/dist/context-strategy.d.ts.map +1 -0
  37. package/dist/context-strategy.js +274 -0
  38. package/dist/context-strategy.js.map +1 -0
  39. package/dist/cost.d.ts +152 -0
  40. package/dist/cost.d.ts.map +1 -0
  41. package/dist/cost.js +377 -0
  42. package/dist/cost.js.map +1 -0
  43. package/dist/credentials/resolver.d.ts +17 -0
  44. package/dist/credentials/resolver.d.ts.map +1 -1
  45. package/dist/credentials/resolver.js +92 -34
  46. package/dist/credentials/resolver.js.map +1 -1
  47. package/dist/discovery/anthropic.d.ts +19 -1
  48. package/dist/discovery/anthropic.d.ts.map +1 -1
  49. package/dist/discovery/anthropic.js +112 -8
  50. package/dist/discovery/anthropic.js.map +1 -1
  51. package/dist/discovery/base.d.ts +7 -0
  52. package/dist/discovery/base.d.ts.map +1 -1
  53. package/dist/discovery/base.js +32 -4
  54. package/dist/discovery/base.js.map +1 -1
  55. package/dist/discovery/bedrock.d.ts.map +1 -1
  56. package/dist/discovery/cerebras.d.ts.map +1 -1
  57. package/dist/discovery/cohere.d.ts.map +1 -1
  58. package/dist/discovery/deepinfra.d.ts.map +1 -1
  59. package/dist/discovery/deepseek.d.ts.map +1 -1
  60. package/dist/discovery/fireworks.d.ts.map +1 -1
  61. package/dist/discovery/glm.d.ts.map +1 -1
  62. package/dist/discovery/google.d.ts +3 -1
  63. package/dist/discovery/google.d.ts.map +1 -1
  64. package/dist/discovery/google.js +13 -6
  65. package/dist/discovery/google.js.map +1 -1
  66. package/dist/discovery/groq.d.ts.map +1 -1
  67. package/dist/discovery/index.d.ts +3 -0
  68. package/dist/discovery/index.d.ts.map +1 -1
  69. package/dist/discovery/index.js +9 -0
  70. package/dist/discovery/index.js.map +1 -1
  71. package/dist/discovery/litellm-seed.js +12 -8
  72. package/dist/discovery/litellm-seed.js.map +1 -1
  73. package/dist/discovery/llama-cpp.d.ts.map +1 -1
  74. package/dist/discovery/lmstudio.d.ts +26 -0
  75. package/dist/discovery/lmstudio.d.ts.map +1 -0
  76. package/dist/discovery/lmstudio.js +93 -0
  77. package/dist/discovery/lmstudio.js.map +1 -0
  78. package/dist/discovery/minimax.d.ts.map +1 -1
  79. package/dist/discovery/mistral.d.ts.map +1 -1
  80. package/dist/discovery/modelsdev-seed.js +16 -12
  81. package/dist/discovery/modelsdev-seed.js.map +1 -1
  82. package/dist/discovery/moonshot.d.ts.map +1 -1
  83. package/dist/discovery/nvidia.d.ts.map +1 -1
  84. package/dist/discovery/ollama.d.ts.map +1 -1
  85. package/dist/discovery/openai-compatible.d.ts.map +1 -1
  86. package/dist/discovery/openai.d.ts.map +1 -1
  87. package/dist/discovery/openrouter.d.ts +29 -5
  88. package/dist/discovery/openrouter.d.ts.map +1 -1
  89. package/dist/discovery/openrouter.js +56 -18
  90. package/dist/discovery/openrouter.js.map +1 -1
  91. package/dist/discovery/perplexity.d.ts.map +1 -1
  92. package/dist/discovery/promo-overrides.js.map +1 -1
  93. package/dist/discovery/static-direct.d.ts +9 -1
  94. package/dist/discovery/static-direct.d.ts.map +1 -1
  95. package/dist/discovery/static-direct.js +68 -7
  96. package/dist/discovery/static-direct.js.map +1 -1
  97. package/dist/discovery/together.d.ts.map +1 -1
  98. package/dist/discovery/vercel.d.ts +45 -0
  99. package/dist/discovery/vercel.d.ts.map +1 -0
  100. package/dist/discovery/vercel.js +232 -0
  101. package/dist/discovery/vercel.js.map +1 -0
  102. package/dist/discovery/vertex.d.ts.map +1 -1
  103. package/dist/discovery/vllm.d.ts +26 -0
  104. package/dist/discovery/vllm.d.ts.map +1 -0
  105. package/dist/discovery/vllm.js +93 -0
  106. package/dist/discovery/vllm.js.map +1 -0
  107. package/dist/discovery/zai.d.ts.map +1 -1
  108. package/dist/discovery-contract.d.ts +8 -1
  109. package/dist/discovery-contract.d.ts.map +1 -1
  110. package/dist/discovery-contract.js +28 -1
  111. package/dist/discovery-contract.js.map +1 -1
  112. package/dist/discovery-routes.d.ts +9 -0
  113. package/dist/discovery-routes.d.ts.map +1 -1
  114. package/dist/discovery-routes.js +106 -18
  115. package/dist/discovery-routes.js.map +1 -1
  116. package/dist/enrichment/litellm-catalog.d.ts +1 -0
  117. package/dist/enrichment/litellm-catalog.d.ts.map +1 -1
  118. package/dist/enrichment/litellm-catalog.js.map +1 -1
  119. package/dist/enrichment/litellm.d.ts +1 -1
  120. package/dist/enrichment/litellm.d.ts.map +1 -1
  121. package/dist/enrichment/litellm.js +9 -3
  122. package/dist/enrichment/litellm.js.map +1 -1
  123. package/dist/entry.d.ts +14 -0
  124. package/dist/entry.d.ts.map +1 -0
  125. package/dist/entry.js +26 -0
  126. package/dist/entry.js.map +1 -0
  127. package/dist/index.d.ts +10 -0
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +5 -0
  130. package/dist/index.js.map +1 -1
  131. package/dist/mcp-server.d.ts +207 -2
  132. package/dist/mcp-server.d.ts.map +1 -1
  133. package/dist/mcp-server.js +254 -54
  134. package/dist/mcp-server.js.map +1 -1
  135. package/dist/model-features.d.ts.map +1 -1
  136. package/dist/model-features.js +11 -2
  137. package/dist/model-features.js.map +1 -1
  138. package/dist/normalize.d.ts +7 -7
  139. package/dist/normalize.js +8 -8
  140. package/dist/provider-catalog.d.ts +17 -2
  141. package/dist/provider-catalog.d.ts.map +1 -1
  142. package/dist/provider-catalog.js +112 -0
  143. package/dist/provider-catalog.js.map +1 -1
  144. package/dist/proxy.d.ts +49 -7
  145. package/dist/proxy.d.ts.map +1 -1
  146. package/dist/proxy.js +765 -80
  147. package/dist/proxy.js.map +1 -1
  148. package/dist/registry-discovery.d.ts.map +1 -1
  149. package/dist/registry-discovery.js +4 -4
  150. package/dist/registry-discovery.js.map +1 -1
  151. package/dist/registry-query.d.ts +2 -2
  152. package/dist/registry-query.d.ts.map +1 -1
  153. package/dist/registry-query.js +60 -9
  154. package/dist/registry-query.js.map +1 -1
  155. package/dist/registry-routing.d.ts +65 -0
  156. package/dist/registry-routing.d.ts.map +1 -0
  157. package/dist/registry-routing.js +165 -0
  158. package/dist/registry-routing.js.map +1 -0
  159. package/dist/registry-runtime.d.ts +2 -0
  160. package/dist/registry-runtime.d.ts.map +1 -1
  161. package/dist/registry-runtime.js +167 -15
  162. package/dist/registry-runtime.js.map +1 -1
  163. package/dist/registry-selection.d.ts.map +1 -1
  164. package/dist/registry-selection.js +27 -3
  165. package/dist/registry-selection.js.map +1 -1
  166. package/dist/registry.d.ts +69 -0
  167. package/dist/registry.d.ts.map +1 -1
  168. package/dist/registry.js +135 -3
  169. package/dist/registry.js.map +1 -1
  170. package/dist/resilience.d.ts +15 -0
  171. package/dist/resilience.d.ts.map +1 -1
  172. package/dist/resilience.js +23 -1
  173. package/dist/resilience.js.map +1 -1
  174. package/dist/security.js +6 -6
  175. package/dist/security.js.map +1 -1
  176. package/dist/server.d.ts +44 -2
  177. package/dist/server.d.ts.map +1 -1
  178. package/dist/server.js +329 -34
  179. package/dist/server.js.map +1 -1
  180. package/dist/tally.d.ts +80 -0
  181. package/dist/tally.d.ts.map +1 -0
  182. package/dist/tally.js +176 -0
  183. package/dist/tally.js.map +1 -0
  184. package/dist/types.d.ts +64 -3
  185. package/dist/types.d.ts.map +1 -1
  186. package/dist/wire-anthropic.d.ts +264 -0
  187. package/dist/wire-anthropic.d.ts.map +1 -0
  188. package/dist/wire-anthropic.js +960 -0
  189. package/dist/wire-anthropic.js.map +1 -0
  190. package/package.json +106 -97
  191. 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
@@ -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;IAuDlF;;;;;;;;OAQG;YACW,gBAAgB;IAuD9B;;;;;;OAMG;YACW,aAAa;IA2C3B;;;;;;OAMG;YACW,aAAa;IAkD3B;;;;;OAKG;YACW,iBAAiB;IAoB/B;;;OAGG;IACH,OAAO,CAAC,aAAa;IAQrB;;;;;;;;;;;;;OAaG;YACW,wBAAwB;IAqDtC;;;;;;;;;OASG;YACW,gBAAgB;IAsB9B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,kBAAkB;IAkC1B;;;;;;;;;;;;;;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"}
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 claudeJson = await this.readJson(join(this.home, ".claude.json"));
118
- if (claudeJson?.apiKey) {
119
- return { apiKey: claudeJson.apiKey, source: "cli", path: join(this.home, ".claude.json") };
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/claude/settings.json
122
- const claudeSettings = await this.readJson(join(this.home, ".config", "claude", "settings.json"));
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 codexAuth = await this.readJson(join(this.home, ".codex", "auth.json"));
141
- if (codexAuth?.anthropic) {
142
- return { apiKey: codexAuth.anthropic, source: "cli", path: join(this.home, ".codex", "auth.json") };
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 geminiCreds = await this.readJson(join(this.home, ".gemini", "credentials.json"));
212
- if (geminiCreds?.apiKey) {
213
- return {
214
- apiKey: geminiCreds.apiKey,
215
- source: "cli",
216
- path: join(this.home, ".gemini", "credentials.json"),
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["aws_access_key_id"];
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")["region"] ??
356
- this.parseAwsIniSection(configText, "default")["region"];
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
- const header = trimmed.slice(1, trimmed.indexOf("]")).trim();
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
  }