@argosvix/sdk 0.4.2-alpha.1 → 0.4.3-alpha.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.
@@ -0,0 +1,356 @@
1
+ import { scanPolicyViolation } from "./policyScan.js";
2
+ /**
3
+ * Runtime 予算ゲート (= ランタイム制御プレーン Phase 1 の SDK 側 enforce)。
4
+ *
5
+ * `config.budgetGate: true` で opt-in すると、wrap 済み client の LLM 呼び出し
6
+ * 前に backend `/v1/gate/budget` の設定 + 当月消費額をローカル評価し、
7
+ * 月次上限超過なら `ArgosvixBudgetExceededError` を投げて呼び出しを止める。
8
+ *
9
+ * 設計 (= R65b fix bundle 反映):
10
+ * - 設定 + 消費額は TTL (= backend response の ttlSeconds、 default 60s)
11
+ * キャッシュ。呼び出し毎の往復はしない。
12
+ * - stale-while-revalidate: snapshot がある間は stale でも即時評価し、
13
+ * refresh は background で走らせる (= hot path に fetch RTT を乗せない)。
14
+ * 同期 await するのは「snapshot が一度も無い初回」と「fail_closed gate が
15
+ * stale」の 2 場面のみ。
16
+ * - 失敗 backoff: fetch 失敗後 FAILURE_RETRY_MS は再試行しない (= backend
17
+ * 障害中に呼び出し毎 3 秒の blocking fetch を発生させない、R65b SB-1)。
18
+ * - TTL 間の消費は recorder 経由のローカル加算で補正する。fetch 成功時は
19
+ * 「fetch 開始時点の補正量」だけ差し引く (= fetch 中に積まれた分を
20
+ * 握り潰さない、R65b M-1)。
21
+ * - 時刻は monotonic clock (performance.now) 基準 (= NTP step / VM resume の
22
+ * 時計逆行で cache が無限 fresh になる事故の防止、R65b M-4)。
23
+ * - fail_open (default) = backend 不達 / 設定未取得時は通す。
24
+ * - fail_closed = backend の gate 設定が fail_closed のとき、 cache が
25
+ * MAX_STALE_MULTIPLIER × TTL を超えて stale なら
26
+ * ArgosvixBudgetGateUnavailableError。初回取得前の挙動は
27
+ * `config.budgetGateFailClosed` (default false) で選ぶ。
28
+ * - ゲートで止めた呼び出しも wrapper 経由で error record として ingest
29
+ * される (= 「何件止めたか」 が dashboard / chat で見える)。
30
+ *
31
+ * 既知の limitation (= Phase 1 時点):
32
+ * - SDK が wrap していない method (= Anthropic `messages.stream()`、 Python の
33
+ * `generate_content_stream` 等) は観測も enforce も対象外。詳細は
34
+ * types.ts の budgetGate doc comment 参照。
35
+ */
36
+ const DEFAULT_GATE_ENDPOINT = "https://ingest.argosvix.com/v1/gate/config";
37
+ const INGEST_SUFFIX = "/v1/ingest";
38
+ const FETCH_TIMEOUT_MS = 3_000;
39
+ const DEFAULT_TTL_MS = 60_000;
40
+ // fetch 失敗後の最小再試行間隔 (= 障害中の hot path 保護)
41
+ const FAILURE_RETRY_MS = 30_000;
42
+ // cache がこの倍数 × TTL を超えて refresh できていなければ「状態不明」扱い
43
+ const MAX_STALE_MULTIPLIER = 5;
44
+ /**
45
+ * model 名の canonical form (= R72b MEDIUM 3)。 Gemini 系 SDK が保持する
46
+ * "models/gemini-pro" の provider prefix を除去し、 allowlist 比較を
47
+ * TS / Python 両 SDK で同一にする。 export = test + Python 側と並走 verify 用。
48
+ */
49
+ export function canonicalizeModelName(model) {
50
+ return model.startsWith("models/") ? model.slice("models/".length) : model;
51
+ }
52
+ export class ArgosvixBudgetExceededError extends Error {
53
+ spentUsd;
54
+ limitUsd;
55
+ constructor(spentUsd, limitUsd) {
56
+ super(`[argosvix] budget gate: monthly spend $${spentUsd.toFixed(4)} reached the limit $${limitUsd.toFixed(2)}; call blocked before the provider request`);
57
+ this.name = "ArgosvixBudgetExceededError";
58
+ this.spentUsd = spentUsd;
59
+ this.limitUsd = limitUsd;
60
+ }
61
+ }
62
+ export class ArgosvixBudgetGateUnavailableError extends Error {
63
+ constructor() {
64
+ super("[argosvix] runtime gate: gate state is unavailable and enforce mode is fail_closed; call blocked");
65
+ this.name = "ArgosvixBudgetGateUnavailableError";
66
+ }
67
+ }
68
+ export class ArgosvixPolicyViolationError extends Error {
69
+ reason;
70
+ detail;
71
+ constructor(reason, detail) {
72
+ super(`[argosvix] policy gate: ${detail}; call blocked before the provider request`);
73
+ this.name = "ArgosvixPolicyViolationError";
74
+ this.reason = reason;
75
+ this.detail = detail;
76
+ }
77
+ }
78
+ export class RuntimeGate {
79
+ config;
80
+ snapshot = null;
81
+ inflight = null;
82
+ lastAttemptMs = null;
83
+ lastAttemptFailed = false;
84
+ warnedEndpoint = false;
85
+ warnedNoPolicy = false;
86
+ /** 直近の成功 fetch 以降にこのプロセスが record した消費の補正値。 */
87
+ localSpendUsd = 0;
88
+ constructor(config) {
89
+ this.config = config;
90
+ }
91
+ /** monotonic clock。wall clock の逆行 (NTP step / VM resume) に影響されない。 */
92
+ now() {
93
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
94
+ }
95
+ /** opt-in + 送信可能な構成のときだけ動く。それ以外は完全 no-op。 */
96
+ get active() {
97
+ return ((this.config.budgetGate === true || this.config.policyGate === true) &&
98
+ this.config.disabled !== true &&
99
+ typeof this.config.apiKey === "string" &&
100
+ this.config.apiKey.length > 0);
101
+ }
102
+ /** recorder.record() から呼ばれる。TTL 窓内のローカル消費補正。 */
103
+ noteSpend(costUsd) {
104
+ if (this.config.budgetGate !== true || !this.active)
105
+ return;
106
+ if (Number.isFinite(costUsd) && costUsd > 0) {
107
+ this.localSpendUsd += costUsd;
108
+ }
109
+ }
110
+ /**
111
+ * LLM 呼び出し直前の enforce。超過なら throw、それ以外は素通し。
112
+ * backend 障害は enforce mode に従って fail-open / fail-closed。
113
+ */
114
+ async check(ctx = {}) {
115
+ if (!this.active)
116
+ return;
117
+ const snap = this.snapshot;
118
+ if (!snap) {
119
+ // 初回 (または未取得)。backoff 窓内なら fetch せず素通し判定へ。
120
+ if (this.attemptAllowed())
121
+ await this.refresh();
122
+ }
123
+ else if (this.isStale(snap)) {
124
+ if (this.attemptAllowed()) {
125
+ const p = this.refresh();
126
+ const anyFailClosed = (this.config.budgetGate === true &&
127
+ snap.gate?.enforceMode === "fail_closed") ||
128
+ (this.config.policyGate === true &&
129
+ snap.policy?.enforceMode === "fail_closed");
130
+ if (anyFailClosed) {
131
+ // fail_closed は stale で block し得るため同期 refresh で正確性優先
132
+ await p;
133
+ }
134
+ else {
135
+ // fail_open は stale-while-revalidate (= hot path に RTT を乗せない)
136
+ p.catch(() => { });
137
+ }
138
+ }
139
+ }
140
+ this.evaluate(ctx);
141
+ }
142
+ evaluate(ctx) {
143
+ const snap = this.snapshot;
144
+ if (!snap) {
145
+ // 一度も取得できていない = server 側 enforce mode が不明。
146
+ // local opt-in (budgetGateFailClosed / policyGateFailClosed) でのみ
147
+ // fail_closed に倒す (= R72b HIGH 1: backend が fail_closed 設定でも
148
+ // cold start では SDK がそれを知れないため、 厳格運用は明示 opt-in)。
149
+ if ((this.config.budgetGateFailClosed === true ||
150
+ this.config.policyGateFailClosed === true) &&
151
+ this.lastAttemptFailed) {
152
+ throw new ArgosvixBudgetGateUnavailableError();
153
+ }
154
+ return;
155
+ }
156
+ const ageMs = this.now() - snap.fetchedAtMs;
157
+ const isUnknownStale = ageMs < 0 || ageMs > snap.ttlMs * MAX_STALE_MULTIPLIER;
158
+ if (this.config.budgetGate === true && snap.gate) {
159
+ const gate = snap.gate;
160
+ if (isUnknownStale && gate.enforceMode === "fail_closed") {
161
+ throw new ArgosvixBudgetGateUnavailableError();
162
+ }
163
+ const spent = snap.spentUsd + this.localSpendUsd;
164
+ if (spent >= gate.limitUsd) {
165
+ throw new ArgosvixBudgetExceededError(spent, gate.limitUsd);
166
+ }
167
+ }
168
+ if (this.config.policyGate === true && snap.policy) {
169
+ const policy = snap.policy;
170
+ if (isUnknownStale && policy.enforceMode === "fail_closed") {
171
+ throw new ArgosvixBudgetGateUnavailableError();
172
+ }
173
+ this.evaluatePolicy(policy, ctx);
174
+ }
175
+ }
176
+ /** ポリシーゲートのローカル評価 (= モデル allowlist / PII / secret)。 */
177
+ evaluatePolicy(policy, ctx) {
178
+ const model = ctx.model ??
179
+ (ctx.payload && typeof ctx.payload === "object"
180
+ ? ctx.payload.model
181
+ : undefined);
182
+ if (policy.allow) {
183
+ if (typeof model === "string" && model.length > 0) {
184
+ if (!policy.allow.has(canonicalizeModelName(model))) {
185
+ // model 名は payload 由来の任意文字列になり得るため、 error message には
186
+ // 許可 charset のみ + 128 文字 cap で sanitize して載せる (= prompt 等の
187
+ // 平文が error record に混入しない構造防御、 R66b SB-1 系)。
188
+ const safeModel = model.replace(/[^A-Za-z0-9._:/-]/g, "?").slice(0, 128);
189
+ throw new ArgosvixPolicyViolationError("model_not_allowed", `model "${safeModel}" is not in the configured allowlist`);
190
+ }
191
+ }
192
+ else if (policy.enforceMode === "fail_closed") {
193
+ // QA M-5: allowlist 設定済 + model 解決不能 (= ctx.model も payload.model も
194
+ // 取れない経路) のとき、 fail_open は従来どおり素通し (= false positive 厳禁の
195
+ // 既定) だが、 fail_closed なら「許可リストを照合できない呼び出しは止める」
196
+ // 強制意味論で block する (= 旧実装の no-block 穴を opt-in で塞ぐ)。
197
+ throw new ArgosvixPolicyViolationError("model_not_allowed", "request model could not be resolved to check against the allowlist (fail_closed)");
198
+ }
199
+ }
200
+ if ((policy.blockPii || policy.blockSecrets) && ctx.payload !== undefined) {
201
+ const hit = scanPolicyViolation(ctx.payload, {
202
+ pii: policy.blockPii,
203
+ secrets: policy.blockSecrets,
204
+ });
205
+ if (hit) {
206
+ throw new ArgosvixPolicyViolationError(hit.kind === "secret" ? "secret_detected" : "pii_detected", `request payload contains ${hit.kind === "secret" ? "a credential-like token" : "PII"} (${hit.pattern}) at ${hit.path || "payload"} ("${hit.snippet}")`);
207
+ }
208
+ }
209
+ }
210
+ isStale(snap) {
211
+ const age = this.now() - snap.fetchedAtMs;
212
+ return age < 0 || age >= snap.ttlMs;
213
+ }
214
+ /** 直近 fetch が失敗していた場合、FAILURE_RETRY_MS 経過まで再試行しない。 */
215
+ attemptAllowed() {
216
+ if (this.lastAttemptMs === null || !this.lastAttemptFailed)
217
+ return true;
218
+ return this.now() - this.lastAttemptMs >= FAILURE_RETRY_MS;
219
+ }
220
+ refresh() {
221
+ if (!this.inflight) {
222
+ this.lastAttemptMs = this.now();
223
+ this.inflight = this.fetchOnce().finally(() => {
224
+ this.inflight = null;
225
+ });
226
+ }
227
+ return this.inflight;
228
+ }
229
+ /**
230
+ * gate endpoint の決定。 ingest endpoint を override している場合は同 prefix
231
+ * から導出し、 導出できない形 (= /v1/ingest で終わらない custom URL) なら
232
+ * gate を無効化する (= 他環境向け Bearer key を production に送らない、R65b M-2)。
233
+ */
234
+ gateEndpoint() {
235
+ if (this.config.gateEndpoint)
236
+ return this.config.gateEndpoint;
237
+ const endpoint = this.config.endpoint;
238
+ if (endpoint) {
239
+ if (endpoint.endsWith(INGEST_SUFFIX)) {
240
+ return endpoint.slice(0, -INGEST_SUFFIX.length) + "/v1/gate/config";
241
+ }
242
+ if (!this.warnedEndpoint) {
243
+ this.warnedEndpoint = true;
244
+ // eslint-disable-next-line no-console
245
+ console.warn("[argosvix] budget gate: cannot derive gate endpoint from custom ingest endpoint; set config.gateEndpoint explicitly. Gate enforcement is disabled.");
246
+ }
247
+ return null;
248
+ }
249
+ return DEFAULT_GATE_ENDPOINT;
250
+ }
251
+ async fetchOnce() {
252
+ const url = this.gateEndpoint();
253
+ if (url === null) {
254
+ this.lastAttemptFailed = true;
255
+ return;
256
+ }
257
+ // fetch 中に noteSpend された分を reset で握り潰さないため、開始時点の
258
+ // 補正量を捕捉して成功時に差分減算する (R65b M-1)。
259
+ const localAtStart = this.localSpendUsd;
260
+ try {
261
+ const controller = new AbortController();
262
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
263
+ let res;
264
+ try {
265
+ res = await fetch(url, {
266
+ method: "GET",
267
+ headers: { Authorization: `Bearer ${this.config.apiKey}` },
268
+ signal: controller.signal,
269
+ });
270
+ }
271
+ finally {
272
+ clearTimeout(timer);
273
+ }
274
+ if (!res.ok) {
275
+ // body を消費して connection 再利用を阻害しない (R65b L-1)
276
+ await res.arrayBuffer().catch(() => { });
277
+ this.lastAttemptFailed = true;
278
+ return; // 旧 snapshot keep、mode に応じて evaluate() 側で判断
279
+ }
280
+ const raw = (await res.json());
281
+ // /v1/gate/config 形 { budget: {...}, policy: {...}, ttlSeconds } を
282
+ // 第一に、 旧 /v1/gate/budget 形 { gates, spentUsdThisMonth, ttlSeconds }
283
+ // (= gateEndpoint override で budget endpoint を指す構成) も受理する。
284
+ const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
285
+ const root = isObj(raw) ? raw : {};
286
+ const budgetPart = isObj(root["budget"]) ? root["budget"] : root;
287
+ const policyPart = isObj(root["policy"]) ? root["policy"] : null;
288
+ if (this.config.policyGate === true &&
289
+ !("policy" in root) &&
290
+ !this.warnedNoPolicy) {
291
+ // 旧 /v1/gate/budget shape に gateEndpoint を向けた構成では policy が
292
+ // 取得できず silent no-op になるため 1 回だけ告知 (= R66b LOW 3)。
293
+ this.warnedNoPolicy = true;
294
+ // eslint-disable-next-line no-console
295
+ console.warn("[argosvix] policy gate: gate endpoint response has no policy section; point gateEndpoint at /v1/gate/config to enable policy enforcement.");
296
+ }
297
+ const gates = Array.isArray(budgetPart["gates"])
298
+ ? budgetPart["gates"]
299
+ : [];
300
+ const accountGate = gates.find((g) => isObj(g) &&
301
+ (g["projectId"] === null || g["projectId"] === undefined) &&
302
+ g["enabled"] === true &&
303
+ typeof g["monthlyLimitUsd"] === "number" &&
304
+ Number.isFinite(g["monthlyLimitUsd"])) ?? null;
305
+ let policy = null;
306
+ if (policyPart && policyPart["enabled"] === true) {
307
+ const allowRaw = policyPart["modelAllowlist"];
308
+ // R72b MEDIUM 3 fix: allowlist 側も candidate 側も canonical form
309
+ // (= 先頭 "models/" を除去) で比較する。 Gemini legacy SDK は model 名を
310
+ // "models/gemini-pro" 形式で保持する実装があり (= Python の model_name)、
311
+ // TS (= 入力値そのまま) と片側だけ block される drift があった。
312
+ const allow = Array.isArray(allowRaw)
313
+ ? new Set(allowRaw
314
+ .filter((m) => typeof m === "string")
315
+ .map(canonicalizeModelName))
316
+ : null;
317
+ policy = {
318
+ allow: allow && allow.size > 0 ? allow : null,
319
+ blockPii: policyPart["blockPii"] === true,
320
+ blockSecrets: policyPart["blockSecrets"] === true,
321
+ enforceMode: policyPart["enforceMode"] === "fail_closed" ? "fail_closed" : "fail_open",
322
+ };
323
+ }
324
+ const ttlSeconds = root["ttlSeconds"] ?? budgetPart["ttlSeconds"];
325
+ const spentRaw = budgetPart["spentUsdThisMonth"];
326
+ this.snapshot = {
327
+ fetchedAtMs: this.now(),
328
+ ttlMs: typeof ttlSeconds === "number" &&
329
+ Number.isFinite(ttlSeconds) &&
330
+ ttlSeconds > 0
331
+ ? ttlSeconds * 1000
332
+ : DEFAULT_TTL_MS,
333
+ gate: accountGate
334
+ ? {
335
+ limitUsd: accountGate["monthlyLimitUsd"],
336
+ enforceMode: accountGate["enforceMode"] === "fail_closed"
337
+ ? "fail_closed"
338
+ : "fail_open",
339
+ }
340
+ : null,
341
+ spentUsd: typeof spentRaw === "number" && Number.isFinite(spentRaw) ? spentRaw : 0,
342
+ policy,
343
+ };
344
+ this.localSpendUsd = Math.max(0, this.localSpendUsd - localAtStart);
345
+ this.lastAttemptFailed = false;
346
+ }
347
+ catch {
348
+ // network / timeout / JSON 不正 = 旧 snapshot keep。fail_open なら素通し、
349
+ // fail_closed は evaluate() の stale 判定で止まる。
350
+ this.lastAttemptFailed = true;
351
+ }
352
+ }
353
+ }
354
+ // Phase 1 で公開した名前の互換 alias (= budget 専用だった頃の命名)。
355
+ export { RuntimeGate as BudgetGate };
356
+ //# sourceMappingURL=budgetGate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budgetGate.js","sourceRoot":"","sources":["../src/budgetGate.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -375,6 +375,7 @@ function wrapOpenAIChat(client, recorder, config) {
375
375
  const id = generateId();
376
376
  const isStream = requestArgs.stream === true;
377
377
  try {
378
+ await recorder.budgetGate.check({ model: requestArgs.model, payload: requestArgs });
378
379
  const response = await originalCreate(...args);
379
380
  if (isStream) {
380
381
  return wrapOpenAIStream(response, recorder, requestArgs, start, id, config);
@@ -515,9 +516,33 @@ function wrapOpenAIResponses(client, recorder, config) {
515
516
  // Note: responses API streaming support is deferred to a later phase.
516
517
  // eslint-disable-next-line no-console
517
518
  console.warn("[argosvix] responses.create with stream:true is not yet observed");
519
+ // gate block も error record として観測する (= R65b H-3、 非 stream 経路の
520
+ // catch と同じ narrative)。
521
+ try {
522
+ await recorder.budgetGate.check({ model: requestArgs.model, payload: requestArgs });
523
+ }
524
+ catch (err) {
525
+ recorder.record({
526
+ id,
527
+ provider: "openai",
528
+ model: requestArgs.model || "unknown",
529
+ promptTokens: 0,
530
+ completionTokens: 0,
531
+ totalTokens: 0,
532
+ costUsd: 0,
533
+ latencyMs: Date.now() - start,
534
+ timestamp: new Date().toISOString(),
535
+ tags: { ...(config.tags ?? {}) },
536
+ ...buildTraceMeta(config),
537
+ error: err instanceof Error ? err.message : String(err),
538
+ requestMeta: buildResponsesRequestMeta(requestArgs),
539
+ });
540
+ throw err;
541
+ }
518
542
  return originalCreate(...args);
519
543
  }
520
544
  try {
545
+ await recorder.budgetGate.check({ model: requestArgs.model, payload: requestArgs });
521
546
  const response = (await originalCreate(...args));
522
547
  const latencyMs = Date.now() - start;
523
548
  const model = response.model || requestArgs.model || "unknown";
@@ -598,6 +623,7 @@ function wrapAnthropic(client, recorder, config) {
598
623
  const id = generateId();
599
624
  const isStream = requestArgs.stream === true;
600
625
  try {
626
+ await recorder.budgetGate.check({ model: requestArgs.model, payload: requestArgs });
601
627
  const response = await originalCreate(...args);
602
628
  if (isStream) {
603
629
  return wrapAnthropicStream(response, recorder, requestArgs, start, id, config);
@@ -731,6 +757,7 @@ function wrapMistral(client, recorder, config) {
731
757
  const requestArgs = args[0] || {};
732
758
  const id = generateId();
733
759
  try {
760
+ await recorder.budgetGate.check({ model: requestArgs.model, payload: requestArgs });
734
761
  const response = (await originalComplete(...args));
735
762
  const latencyMs = Date.now() - start;
736
763
  const model = response.model || requestArgs.model || "unknown";
@@ -780,6 +807,7 @@ function wrapMistral(client, recorder, config) {
780
807
  const requestArgs = args[0] || {};
781
808
  const id = generateId();
782
809
  try {
810
+ await recorder.budgetGate.check({ model: requestArgs.model, payload: requestArgs });
783
811
  const stream = (await originalStream(...args));
784
812
  return wrapMistralStream(stream, recorder, requestArgs, start, id, config);
785
813
  }
@@ -897,6 +925,7 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
897
925
  const id = generateId();
898
926
  const requestArgs = args[0];
899
927
  try {
928
+ await recorder.budgetGate.check({ model: modelName, payload: requestArgs });
900
929
  const result = (await originalGenerate(...args));
901
930
  const usage = result.response?.usageMetadata;
902
931
  const promptTokens = usage?.promptTokenCount ?? 0;
@@ -945,6 +974,7 @@ function wrapGeminiLegacyModel(model, modelName, recorder, config) {
945
974
  const id = generateId();
946
975
  const requestArgs = args[0];
947
976
  try {
977
+ await recorder.budgetGate.check({ model: modelName, payload: requestArgs });
948
978
  const result = (await originalStream(...args));
949
979
  if (!result.stream)
950
980
  return result;
@@ -1033,6 +1063,7 @@ function wrapGeminiNew(client, recorder, config) {
1033
1063
  const requestArgs = args[0] || {};
1034
1064
  const modelName = requestArgs.model || "unknown";
1035
1065
  try {
1066
+ await recorder.budgetGate.check({ model: modelName, payload: requestArgs });
1036
1067
  const result = (await originalGenerate(...args));
1037
1068
  const usage = result.usageMetadata;
1038
1069
  const promptTokens = usage?.promptTokenCount ?? 0;
@@ -1082,6 +1113,7 @@ function wrapGeminiNew(client, recorder, config) {
1082
1113
  const requestArgs = args[0] || {};
1083
1114
  const modelName = requestArgs.model || "unknown";
1084
1115
  try {
1116
+ await recorder.budgetGate.check({ model: modelName, payload: requestArgs });
1085
1117
  const stream = (await originalStream(...args));
1086
1118
  // C-2 + H-3 fix: AsyncGenerator wrap with finally-record + error tied to consumption
1087
1119
  return (async function* () {
@@ -1181,10 +1213,32 @@ function buildGeminiNewRequestMeta(requestArgs) {
1181
1213
  return meta;
1182
1214
  }
1183
1215
  /**
1184
- * Lightweight ID generator (not strictly ULID-compatible). Will be swapped for
1185
- * the `ulid` library in a later release.
1216
+ * Time-ordered record ID generator.
1217
+ *
1218
+ * audit round2 M26 fix = 旧実装は `Date.now()` prefix + `Math.random().toString(36).slice(2,10)`
1219
+ * suffix で、 (1) 非暗号乱数 (2) 値次第で suffix が 8 文字未満になり実効エントロピーが
1220
+ * 41bit を大きく下回る、 という二重の弱さがあった。 backend は
1221
+ * `INSERT ... ON CONFLICT(account_id, id) DO NOTHING` で衝突を無音 skip するため、
1222
+ * 衝突した本物の call が課金・quota・観測から消える。 暗号乱数 (crypto.getRandomValues /
1223
+ * randomUUID) で 128bit 級の衝突耐性に引き上げる。 先頭に時刻 prefix を残して時系列順も保つ。
1186
1224
  */
1187
1225
  function generateId() {
1188
- return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
1226
+ const timePrefix = Date.now().toString(36);
1227
+ // globalThis.crypto は Workers / Node 18+ / 近代ブラウザで利用可能。 SDK tsconfig は DOM lib を
1228
+ // 含めないため Crypto 型に依存せず構造的に access する。
1229
+ const c = globalThis.crypto;
1230
+ if (c?.randomUUID) {
1231
+ return `${timePrefix}-${c.randomUUID()}`;
1232
+ }
1233
+ if (c?.getRandomValues) {
1234
+ const bytes = new Uint8Array(16);
1235
+ c.getRandomValues(bytes);
1236
+ let hex = "";
1237
+ for (const b of bytes)
1238
+ hex += b.toString(16).padStart(2, "0");
1239
+ return `${timePrefix}-${hex}`;
1240
+ }
1241
+ // crypto 不在 runtime の最終 fallback (= 旧来挙動より広い乱数 2 連結で衝突確率を下げる)。
1242
+ return `${timePrefix}-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
1189
1243
  }
1190
1244
  //# sourceMappingURL=client.js.map