@kenkaiiii/gg-ai 5.76.0 → 5.78.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/dist/index.js CHANGED
@@ -38,7 +38,7 @@ var ProviderError = class extends GGAIError {
38
38
  resetsAt;
39
39
  constructor(provider, message, options) {
40
40
  super(message, {
41
- source: "provider",
41
+ source: options?.source ?? "provider",
42
42
  requestId: options?.requestId,
43
43
  hint: options?.hint,
44
44
  cause: options?.cause
@@ -435,6 +435,46 @@ var StreamResult = class _StreamResult {
435
435
  }
436
436
  };
437
437
 
438
+ // src/utils/provider-options.ts
439
+ var RESERVED_BODY_FIELDS = /* @__PURE__ */ new Set([
440
+ "contents",
441
+ "dimensions",
442
+ "documents",
443
+ "encoding_format",
444
+ "input",
445
+ "messages",
446
+ "model",
447
+ "query",
448
+ "requests",
449
+ "stream",
450
+ "system",
451
+ "tools",
452
+ "top_n",
453
+ // Never copy prototype-shaped keys onto a request body.
454
+ "__proto__",
455
+ "constructor",
456
+ "prototype"
457
+ ]);
458
+ function mergeProviderOptions(body, providerOptions) {
459
+ if (!providerOptions) return body;
460
+ const base = body;
461
+ const extra = {};
462
+ for (const key of Object.keys(providerOptions).sort()) {
463
+ if (RESERVED_BODY_FIELDS.has(key)) continue;
464
+ if (Object.hasOwn(base, key) && base[key] !== void 0) continue;
465
+ const value = providerOptions[key];
466
+ if (value === void 0) continue;
467
+ Object.defineProperty(extra, key, {
468
+ value,
469
+ enumerable: true,
470
+ writable: true,
471
+ configurable: true
472
+ });
473
+ }
474
+ if (Object.keys(extra).length === 0) return body;
475
+ return { ...base, ...extra };
476
+ }
477
+
438
478
  // src/utils/zod-to-json-schema.ts
439
479
  import { z } from "zod";
440
480
  var schemaCache = /* @__PURE__ */ new WeakMap();
@@ -1090,6 +1130,7 @@ function remapAnthropicToolCallId(id, idMap) {
1090
1130
  function toAnthropicMessages(messages, cacheControl, options) {
1091
1131
  let systemText;
1092
1132
  const out = [];
1133
+ const userTurnIdx = [];
1093
1134
  const idMap = /* @__PURE__ */ new Map();
1094
1135
  const keepFallbackBlocks = options?.fallbackBlocks === true;
1095
1136
  const droppedToolCallIds = /* @__PURE__ */ new Set();
@@ -1110,6 +1151,7 @@ function toAnthropicMessages(messages, cacheControl, options) {
1110
1151
  } else if (!msg.content.some((p) => !(p.type === "text" && p.text === ""))) {
1111
1152
  continue;
1112
1153
  }
1154
+ userTurnIdx.push(out.length);
1113
1155
  out.push({
1114
1156
  role: "user",
1115
1157
  content: typeof msg.content === "string" ? msg.content : msg.content.filter((part) => !(part.type === "text" && part.text === "")).map((part) => {
@@ -1164,28 +1206,21 @@ function toAnthropicMessages(messages, cacheControl, options) {
1164
1206
  }
1165
1207
  }
1166
1208
  if (cacheControl && out.length > 0) {
1167
- for (let i = out.length - 1; i >= 0; i--) {
1168
- if (out[i].role === "user") {
1169
- const content = out[i].content;
1170
- if (typeof content === "string") {
1171
- out[i] = {
1172
- role: "user",
1173
- content: [
1174
- {
1175
- type: "text",
1176
- text: content,
1177
- cache_control: cacheControl
1178
- }
1179
- ]
1180
- };
1181
- } else if (Array.isArray(content) && content.length > 0) {
1182
- const last = content[content.length - 1];
1183
- content[content.length - 1] = {
1184
- ...last,
1185
- cache_control: cacheControl
1186
- };
1209
+ let tail = out.length - 1;
1210
+ while (tail >= 0 && out[tail]?.role !== "user") tail--;
1211
+ if (tail !== -1) {
1212
+ if (cacheControl.ttl === "1h") {
1213
+ const anchor = userTurnIdx.at(-1);
1214
+ if (anchor === tail) {
1215
+ const previous = userTurnIdx.at(-2);
1216
+ if (previous !== void 0) markAnthropicCacheBreakpoint(out, previous, cacheControl);
1217
+ markAnthropicCacheBreakpoint(out, tail, cacheControl);
1218
+ } else {
1219
+ if (anchor !== void 0) markAnthropicCacheBreakpoint(out, anchor, cacheControl);
1220
+ markAnthropicCacheBreakpoint(out, tail, { type: "ephemeral" });
1187
1221
  }
1188
- break;
1222
+ } else {
1223
+ markAnthropicCacheBreakpoint(out, tail, cacheControl);
1189
1224
  }
1190
1225
  }
1191
1226
  }
@@ -1212,6 +1247,25 @@ function toAnthropicMessages(messages, cacheControl, options) {
1212
1247
  }
1213
1248
  return { system, messages: out };
1214
1249
  }
1250
+ function markAnthropicCacheBreakpoint(out, index, cacheControl) {
1251
+ const message = out[index];
1252
+ if (!message) return;
1253
+ const content = message.content;
1254
+ if (typeof content === "string") {
1255
+ out[index] = {
1256
+ role: message.role,
1257
+ content: [
1258
+ { type: "text", text: content, cache_control: cacheControl }
1259
+ ]
1260
+ };
1261
+ } else if (content.length > 0) {
1262
+ const last = content[content.length - 1];
1263
+ content[content.length - 1] = {
1264
+ ...last,
1265
+ cache_control: cacheControl
1266
+ };
1267
+ }
1268
+ }
1215
1269
  function toAnthropicTools(tools, options) {
1216
1270
  return tools.map((tool, index) => {
1217
1271
  const anthropicTool = {
@@ -1677,7 +1731,7 @@ async function* runStream(options) {
1677
1731
  }
1678
1732
  maxTokens = 1;
1679
1733
  }
1680
- const params = {
1734
+ const baseParams = {
1681
1735
  model: options.model,
1682
1736
  max_tokens: maxTokens,
1683
1737
  messages,
@@ -1720,6 +1774,7 @@ async function* runStream(options) {
1720
1774
  ...useServerFallback ? { fallbacks: "default" } : {},
1721
1775
  stream: useStreaming
1722
1776
  };
1777
+ const params = mergeProviderOptions(baseParams, options.providerOptions);
1723
1778
  const hasAdaptiveThinking = isAdaptiveThinkingModel(options.model);
1724
1779
  const betaHeaders = [
1725
1780
  ...isOAuth ? ["claude-code-20250219", "oauth-2025-04-20"] : [],
@@ -2425,20 +2480,21 @@ async function* runStream2(options) {
2425
2480
  params.thinking = { type: "disabled" };
2426
2481
  }
2427
2482
  }
2483
+ const requestParams = mergeProviderOptions(params, options.providerOptions);
2428
2484
  if (getEnvironment()?.GGAI_DUMP_REQUEST) {
2429
2485
  const fs = await import("fs");
2430
2486
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2431
2487
  const dumpPath = `/tmp/ggai-request-${ts}.json`;
2432
- fs.writeFileSync(dumpPath, JSON.stringify(params, null, 2));
2488
+ fs.writeFileSync(dumpPath, JSON.stringify(requestParams, null, 2));
2433
2489
  fs.appendFileSync(
2434
2490
  "/tmp/ggai-requests.log",
2435
- `[${ts}] ${dumpPath} messages=${params.messages.length}
2491
+ `[${ts}] ${dumpPath} messages=${requestParams.messages.length}
2436
2492
  `
2437
2493
  );
2438
2494
  }
2439
2495
  if (!useStreaming) {
2440
2496
  try {
2441
- const completion = await client.chat.completions.create(params, {
2497
+ const completion = await client.chat.completions.create(requestParams, {
2442
2498
  signal: options.signal ?? void 0
2443
2499
  });
2444
2500
  yield* synthesizeEventsFromCompletion(completion, !!options.thinking, endpointKey);
@@ -2449,7 +2505,7 @@ async function* runStream2(options) {
2449
2505
  }
2450
2506
  let stream2;
2451
2507
  try {
2452
- stream2 = await client.chat.completions.create(params, {
2508
+ stream2 = await client.chat.completions.create(requestParams, {
2453
2509
  signal: options.signal ?? void 0
2454
2510
  });
2455
2511
  } catch (err) {
@@ -2937,7 +2993,9 @@ async function* runStream3(options, retriedWithoutReasoning = false) {
2937
2993
  headers["session_id"] = transportSessionId;
2938
2994
  headers["x-client-request-id"] = transportSessionId;
2939
2995
  }
2940
- const encodedRequest = await encodeCodexRequest(body);
2996
+ const encodedRequest = await encodeCodexRequest(
2997
+ mergeProviderOptions(body, options.providerOptions)
2998
+ );
2941
2999
  if (encodedRequest.compressed) headers["Content-Encoding"] = "zstd";
2942
3000
  providerDiag("codex_request_body", {
2943
3001
  rawBytes: encodedRequest.rawBytes,
@@ -3652,11 +3710,11 @@ function stripUnsupportedSchemaFields(value) {
3652
3710
  ["exclusiveMinimum", "minimum", 1],
3653
3711
  ["exclusiveMaximum", "maximum", -1]
3654
3712
  ]) {
3655
- const bound = value[key];
3656
- if (bound === void 0) continue;
3713
+ const bound2 = value[key];
3714
+ if (bound2 === void 0) continue;
3657
3715
  delete value[key];
3658
- if (typeof bound === "number" && value[target] === void 0) {
3659
- value[target] = value.type === "integer" ? bound + step : bound;
3716
+ if (typeof bound2 === "number" && value[target] === void 0) {
3717
+ value[target] = value.type === "integer" ? bound2 + step : bound2;
3660
3718
  }
3661
3719
  }
3662
3720
  for (const item of Object.values(value)) {
@@ -3750,7 +3808,7 @@ function buildRequestPlan(options, method) {
3750
3808
  throw new ProviderError("gemini", formatUnsupportedModelMessage(options.model));
3751
3809
  }
3752
3810
  const projectId = getGoogleProject(options);
3753
- const request = buildGenerateRequest(options);
3811
+ const request = mergeProviderOptions(buildGenerateRequest(options), options.providerOptions);
3754
3812
  return {
3755
3813
  url: getCodeAssistEndpoint(method),
3756
3814
  headers: {
@@ -3988,6 +4046,449 @@ function toError3(err) {
3988
4046
  return new ProviderError("gemini", String(err));
3989
4047
  }
3990
4048
 
4049
+ // src/redaction.ts
4050
+ var REDACTED = "[REDACTED]";
4051
+ var TRUNCATED = "[TRUNCATED]";
4052
+ var CIRCULAR = "[CIRCULAR]";
4053
+ var SENSITIVE_NAME = /(?:^|[_-])(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|key|auth(?:orization)?|bearer|cookie|credential|private[_-]?key|password|passwd|secret)(?:$|[_-])/i;
4054
+ var ENV_SECRET_ASSIGNMENT = /\b((?:[A-Z0-9]+_)*(?:API_?KEY|ACCESS_TOKEN|REFRESH_TOKEN|TOKEN|KEY|AUTH|AUTHORIZATION|BEARER|CREDENTIALS?|PASSWORD|PASSWD|SECRET))\b(\s*[=:]\s*)(["']?)(?!\$|process\.env|os\.environ|import\.meta|env\.)(?=[^\s,"';}]*\d)([^\s,"';}=$][^\s,"';}]{7,})\3/g;
4055
+ var COMPACT_SECRET_ASSIGNMENT = /\b((?:[a-z0-9]+[_-])*(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|auth|authorization|credentials?|password|passwd|secret)|(?:[a-z0-9]+[_-])+key)=(["']?)(?!\$)([^\s,"'&;}=][^\s,"'&;}]{7,})\2/gi;
4056
+ var URL_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#@:"'`<>]*:[^\s/?#"'`<>]+@/gi;
4057
+ var URL_USERINFO_TO_FIRST_AT = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]*:[^\s/@]+@/gi;
4058
+ function escaped(value) {
4059
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4060
+ }
4061
+ function normalizedSecrets(secrets) {
4062
+ if (!secrets) return [];
4063
+ return [...new Set([...secrets].filter((value) => value.length >= 8 && value !== REDACTED))].sort(
4064
+ (a, b) => b.length - a.length
4065
+ );
4066
+ }
4067
+ function environmentSecrets(env) {
4068
+ const values = /* @__PURE__ */ new Set();
4069
+ for (const [name, value] of Object.entries(env)) {
4070
+ if (!value || value.length < 8 || value === REDACTED || !SENSITIVE_NAME.test(name)) continue;
4071
+ values.add(value);
4072
+ }
4073
+ return [...values].sort((a, b) => b.length - a.length);
4074
+ }
4075
+ function redactText(text, options = {}) {
4076
+ let result = text;
4077
+ result = result.replace(
4078
+ /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g,
4079
+ REDACTED
4080
+ );
4081
+ result = result.replace(URL_USERINFO, `$1${REDACTED}@`);
4082
+ result = result.replace(URL_USERINFO_TO_FIRST_AT, `$1${REDACTED}@`);
4083
+ result = result.replace(
4084
+ /\b(authorization\s*[:=]\s*)(?:bearer|basic)\s+[^\s,;]+/gi,
4085
+ `$1${REDACTED}`
4086
+ );
4087
+ result = result.replace(
4088
+ /\b(bearer|basic)\s+(?![A-Za-z][a-z]{0,14}\b)[A-Za-z0-9+/_.=-]{8,}/gi,
4089
+ `$1 ${REDACTED}`
4090
+ );
4091
+ result = result.replace(/\b(cookie|set-cookie)(\s*:\s*)[^\s=;:]+=[^\r\n]*/gi, `$1$2${REDACTED}`);
4092
+ result = result.replace(
4093
+ /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g,
4094
+ REDACTED
4095
+ );
4096
+ result = result.replace(
4097
+ /\b(?:sk-(?:ant-|proj-)?|xox[baprs]-|gh[pousr]_|github_pat_|AIza)[A-Za-z0-9_-]{12,}\b/g,
4098
+ REDACTED
4099
+ );
4100
+ result = result.replace(
4101
+ ENV_SECRET_ASSIGNMENT,
4102
+ (_match, name, separator, quote) => `${name}${separator}${quote}${REDACTED}${quote}`
4103
+ );
4104
+ result = result.replace(
4105
+ COMPACT_SECRET_ASSIGNMENT,
4106
+ (_match, name, quote) => `${name}=${quote}${REDACTED}${quote}`
4107
+ );
4108
+ for (const secret of normalizedSecrets(options.secrets)) {
4109
+ result = result.replace(new RegExp(escaped(secret), "g"), REDACTED);
4110
+ }
4111
+ const maxStringLength = options.maxStringLength ?? 1e6;
4112
+ if (result.length > maxStringLength) {
4113
+ result = `${result.slice(0, maxStringLength)}${TRUNCATED}`;
4114
+ }
4115
+ return result;
4116
+ }
4117
+ function isBinary(value) {
4118
+ return value instanceof ArrayBuffer || ArrayBuffer.isView(value) || typeof Blob !== "undefined" && value instanceof Blob;
4119
+ }
4120
+ function isMediaObject(value) {
4121
+ return (value.type === "image" || value.type === "video") && (typeof value.data === "string" || typeof value.url === "string");
4122
+ }
4123
+ function redactValue(value, options = {}) {
4124
+ const maxDepth = options.maxDepth ?? 20;
4125
+ const maxEntries = options.maxEntries ?? 1e4;
4126
+ const ancestors = /* @__PURE__ */ new WeakSet();
4127
+ let entries = 0;
4128
+ const visit = (current, depth, sensitive = false) => {
4129
+ if (typeof current === "string") {
4130
+ if (sensitive && current.length > 0 && current !== REDACTED) return REDACTED;
4131
+ return redactText(current, options);
4132
+ }
4133
+ if (current === null || current === void 0 || typeof current === "number" || typeof current === "boolean" || typeof current === "bigint") {
4134
+ return current;
4135
+ }
4136
+ if (typeof current !== "object") return current;
4137
+ if (isBinary(current)) return current;
4138
+ if (current instanceof Date) return new Date(current.getTime());
4139
+ if (depth >= maxDepth) return TRUNCATED;
4140
+ if (ancestors.has(current)) return CIRCULAR;
4141
+ ancestors.add(current);
4142
+ try {
4143
+ return cloneObject(current, depth);
4144
+ } finally {
4145
+ ancestors.delete(current);
4146
+ }
4147
+ };
4148
+ const cloneObject = (current, depth) => {
4149
+ if (current instanceof Error) {
4150
+ const error = {
4151
+ name: current.name,
4152
+ message: visit(current.message, depth + 1),
4153
+ stack: visit(current.stack, depth + 1)
4154
+ };
4155
+ for (const [key, child] of Object.entries(current)) {
4156
+ error[key] = visit(child, depth + 1, SENSITIVE_NAME.test(key));
4157
+ }
4158
+ return error;
4159
+ }
4160
+ if (Array.isArray(current)) {
4161
+ const clone2 = [];
4162
+ for (const child of current) {
4163
+ if (++entries > maxEntries) {
4164
+ clone2.push(TRUNCATED);
4165
+ break;
4166
+ }
4167
+ clone2.push(visit(child, depth + 1));
4168
+ }
4169
+ return clone2;
4170
+ }
4171
+ const record = current;
4172
+ if (isMediaObject(record)) return { ...record };
4173
+ const clone = {};
4174
+ for (const [key, child] of Object.entries(record)) {
4175
+ if (++entries > maxEntries) {
4176
+ clone[TRUNCATED] = true;
4177
+ break;
4178
+ }
4179
+ clone[key] = visit(child, depth + 1, SENSITIVE_NAME.test(key));
4180
+ }
4181
+ return clone;
4182
+ };
4183
+ return visit(value, 0);
4184
+ }
4185
+
4186
+ // src/utils/json-post.ts
4187
+ var MAX_ERROR_MESSAGE_CHARS = 240;
4188
+ function abortError() {
4189
+ return new DOMException("Aborted", "AbortError");
4190
+ }
4191
+ function throwIfAborted(signal) {
4192
+ if (signal?.aborted) throw abortError();
4193
+ }
4194
+ async function postJson(request) {
4195
+ const { provider, signal } = request;
4196
+ const secrets = request.secrets?.filter((s) => typeof s === "string" && s !== "");
4197
+ const clean = (text2) => bound(redactText(text2, { secrets }));
4198
+ const fetchImpl = request.fetch ?? globalThis.fetch;
4199
+ throwIfAborted(signal);
4200
+ let response;
4201
+ try {
4202
+ response = await fetchImpl(request.url, {
4203
+ method: "POST",
4204
+ headers: request.headers,
4205
+ body: JSON.stringify(request.body),
4206
+ signal
4207
+ });
4208
+ } catch (err) {
4209
+ if (signal?.aborted || isAbortError2(err)) throw abortError();
4210
+ const detail = err instanceof Error ? err.message : String(err);
4211
+ throw new GGAIError(`Couldn't reach ${provider}: ${clean(detail)}`, {
4212
+ source: "network",
4213
+ cause: err
4214
+ });
4215
+ }
4216
+ const headerRequestId = readHeader(
4217
+ response.headers,
4218
+ "x-request-id",
4219
+ "request-id",
4220
+ "openai-request-id"
4221
+ );
4222
+ let text;
4223
+ try {
4224
+ text = await response.text();
4225
+ } catch (err) {
4226
+ if (signal?.aborted || isAbortError2(err)) throw abortError();
4227
+ throw new GGAIError(`Lost the connection to ${provider} while reading its response.`, {
4228
+ source: "network",
4229
+ cause: err
4230
+ });
4231
+ }
4232
+ if (!response.ok) {
4233
+ throw httpError(provider, response, text, headerRequestId, clean);
4234
+ }
4235
+ try {
4236
+ return {
4237
+ json: JSON.parse(text),
4238
+ ...headerRequestId ? { requestId: headerRequestId } : {}
4239
+ };
4240
+ } catch {
4241
+ throw new ProviderError(provider, `${provider} returned a response that isn't valid JSON.`, {
4242
+ statusCode: response.status,
4243
+ ...headerRequestId ? { requestId: headerRequestId } : {}
4244
+ });
4245
+ }
4246
+ }
4247
+ function httpError(provider, response, text, headerRequestId, clean) {
4248
+ const status = response.status;
4249
+ const parsed = parseErrorBody(text);
4250
+ const rawMessage = parsed.message ?? (parsed.isJson ? "" : text.trim());
4251
+ const message = !rawMessage ? emptyProviderErrorMessage(status) : isRawHtmlErrorEcho(rawMessage) ? providerHtmlErrorMessage(status) : clean(rawMessage);
4252
+ const requestId = headerRequestId ?? parsed.requestId;
4253
+ const base = {
4254
+ statusCode: status,
4255
+ ...requestId ? { requestId } : {}
4256
+ };
4257
+ if (status === 401 || status === 403) {
4258
+ return new ProviderError(provider, message, { ...base, source: "auth" });
4259
+ }
4260
+ const codeType = `${parsed.code ?? ""} ${parsed.type ?? ""}`.toLowerCase();
4261
+ if (status === 402 || codeType.includes("insufficient_quota") || isHardBillingMessage(message)) {
4262
+ const stamped = /usage limit reached/i.test(message) ? message : `usage limit reached: ${message}`;
4263
+ return new ProviderError(provider, stamped, base);
4264
+ }
4265
+ if (status === 429) {
4266
+ const retryAfter = Number(readHeader(response.headers, "retry-after"));
4267
+ const resetsAt = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.floor(Date.now() / 1e3) + Math.ceil(retryAfter) : void 0;
4268
+ return new ProviderError(provider, message, {
4269
+ ...base,
4270
+ ...resetsAt !== void 0 ? { resetsAt } : {}
4271
+ });
4272
+ }
4273
+ return new ProviderError(provider, message, base);
4274
+ }
4275
+ function parseErrorBody(text) {
4276
+ let parsed;
4277
+ try {
4278
+ parsed = JSON.parse(text);
4279
+ } catch {
4280
+ return { isJson: false };
4281
+ }
4282
+ if (!isRecord(parsed)) return { isJson: true };
4283
+ const error = parsed.error;
4284
+ const errorObj = isRecord(error) ? error : void 0;
4285
+ const message = stringField(errorObj, "message") ?? (typeof error === "string" && error.trim() ? error.trim() : void 0) ?? stringField(parsed, "message") ?? stringField(parsed, "detail");
4286
+ const requestId = stringField(parsed, "request_id") ?? stringField(errorObj, "request_id");
4287
+ const code = stringField(errorObj, "code") ?? stringField(errorObj, "status");
4288
+ const type = stringField(errorObj, "type");
4289
+ return {
4290
+ isJson: true,
4291
+ ...message ? { message } : {},
4292
+ ...requestId ? { requestId } : {},
4293
+ ...code ? { code } : {},
4294
+ ...type ? { type } : {}
4295
+ };
4296
+ }
4297
+ function stringField(record, key) {
4298
+ const value = record?.[key];
4299
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
4300
+ }
4301
+ function isRecord(value) {
4302
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4303
+ }
4304
+ function isAbortError2(err) {
4305
+ return (err instanceof Error || err instanceof DOMException) && err.name === "AbortError";
4306
+ }
4307
+ function bound(text) {
4308
+ const trimmed = text.trim();
4309
+ return trimmed.length > MAX_ERROR_MESSAGE_CHARS ? `${trimmed.slice(0, MAX_ERROR_MESSAGE_CHARS)}\u2026` : trimmed;
4310
+ }
4311
+ function joinUrl(baseUrl, path) {
4312
+ return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
4313
+ }
4314
+
4315
+ // src/providers/gemini-embeddings.ts
4316
+ var GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
4317
+ var GEMINI_EMBED_BATCH_SIZE = 100;
4318
+ var TASK_TYPES = {
4319
+ query: "RETRIEVAL_QUERY",
4320
+ document: "RETRIEVAL_DOCUMENT"
4321
+ };
4322
+ function supportsTaskType(model) {
4323
+ return !/^gemini-embedding-2(?:$|[-.@])/.test(model);
4324
+ }
4325
+ async function embedGemini(request) {
4326
+ if (!request.apiKey) {
4327
+ throw new GGAIError("Gemini embeddings need a Gemini API key (apiKey).", {
4328
+ source: "auth",
4329
+ hint: "Create a key in Google AI Studio. Gemini Code Assist sign-in can't be used for embeddings."
4330
+ });
4331
+ }
4332
+ const model = request.model.replace(/^models\//, "");
4333
+ const resource = `models/${model}`;
4334
+ const taskType = request.inputType && supportsTaskType(model) ? TASK_TYPES[request.inputType] : void 0;
4335
+ const embedContentConfig = {
4336
+ ...taskType ? { taskType } : {},
4337
+ ...request.dimensions !== void 0 ? { outputDimensionality: request.dimensions } : {}
4338
+ };
4339
+ const body = mergeProviderOptions(
4340
+ {
4341
+ requests: request.input.map((text) => ({
4342
+ model: resource,
4343
+ content: { parts: [{ text }] },
4344
+ ...Object.keys(embedContentConfig).length > 0 ? { embedContentConfig } : {}
4345
+ }))
4346
+ },
4347
+ request.providerOptions
4348
+ );
4349
+ const { json, requestId } = await postJson({
4350
+ provider: "gemini",
4351
+ url: joinUrl(request.baseUrl ?? GEMINI_API_BASE_URL, `${resource}:batchEmbedContents`),
4352
+ headers: { "Content-Type": "application/json", "x-goog-api-key": request.apiKey },
4353
+ body,
4354
+ fetch: request.fetch,
4355
+ signal: request.signal,
4356
+ secrets: [request.apiKey]
4357
+ });
4358
+ const invalid = (detail) => new ProviderError("gemini", `Invalid embeddings response: ${detail}.`, {
4359
+ ...requestId ? { requestId } : {}
4360
+ });
4361
+ if (!isRecord(json) || !Array.isArray(json.embeddings)) {
4362
+ throw invalid("missing `embeddings` array");
4363
+ }
4364
+ const embeddings = json.embeddings.map((entry) => {
4365
+ if (!isRecord(entry) || !Array.isArray(entry.values)) {
4366
+ throw invalid("an entry has no `values` array");
4367
+ }
4368
+ return entry.values;
4369
+ });
4370
+ const usage = json.usageMetadata;
4371
+ const promptTokens = isRecord(usage) ? usage.promptTokenCount : void 0;
4372
+ return {
4373
+ embeddings,
4374
+ model,
4375
+ ...typeof promptTokens === "number" && Number.isFinite(promptTokens) ? { usage: { inputTokens: promptTokens } } : {}
4376
+ };
4377
+ }
4378
+
4379
+ // src/providers/openai-compat-retrieval.ts
4380
+ function jsonHeaders(apiKey) {
4381
+ return {
4382
+ "Content-Type": "application/json",
4383
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
4384
+ };
4385
+ }
4386
+ async function embedOpenAICompatible(request, config) {
4387
+ const body = mergeProviderOptions(
4388
+ {
4389
+ model: config.wireModel ?? request.model,
4390
+ input: request.input,
4391
+ // gg-ai parses float arrays only; never let a server default to base64.
4392
+ encoding_format: "float",
4393
+ ...request.dimensions !== void 0 ? { dimensions: request.dimensions } : {},
4394
+ ...request.inputType && config.inputTypeFields ? config.inputTypeFields(request.inputType) : {}
4395
+ },
4396
+ request.providerOptions
4397
+ );
4398
+ const { json, requestId } = await postJson({
4399
+ provider: config.provider,
4400
+ url: joinUrl(config.baseUrl, "embeddings"),
4401
+ headers: jsonHeaders(request.apiKey),
4402
+ body,
4403
+ fetch: request.fetch,
4404
+ signal: request.signal,
4405
+ secrets: [request.apiKey]
4406
+ });
4407
+ const invalid = (detail) => new ProviderError(config.provider, `Invalid embeddings response: ${detail}.`, {
4408
+ ...requestId ? { requestId } : {}
4409
+ });
4410
+ if (!isRecord(json) || !Array.isArray(json.data)) throw invalid("missing `data` array");
4411
+ const items = json.data.map((item) => {
4412
+ if (!isRecord(item)) throw invalid("an entry in `data` is not an object");
4413
+ const index = item.index;
4414
+ if (index !== void 0 && !(typeof index === "number" && Number.isInteger(index))) {
4415
+ throw invalid("an entry has a non-integer `index`");
4416
+ }
4417
+ if (!Array.isArray(item.embedding)) {
4418
+ throw invalid("an entry's `embedding` is not an array of numbers");
4419
+ }
4420
+ return { index, embedding: item.embedding };
4421
+ });
4422
+ const withIndex = items.filter((item) => item.index !== void 0).length;
4423
+ let embeddings;
4424
+ if (withIndex === 0) {
4425
+ embeddings = items.map((item) => item.embedding);
4426
+ } else if (withIndex !== items.length) {
4427
+ throw invalid("some entries have an `index` and some don't");
4428
+ } else {
4429
+ const slots = new Array(items.length);
4430
+ for (const item of items) {
4431
+ const index = item.index;
4432
+ if (index < 0 || index >= items.length || slots[index] !== void 0) {
4433
+ throw invalid(`unexpected or duplicate index ${index}`);
4434
+ }
4435
+ slots[index] = item.embedding;
4436
+ }
4437
+ embeddings = slots;
4438
+ }
4439
+ const usage = json.usage;
4440
+ const promptTokens = isRecord(usage) ? usage.prompt_tokens : void 0;
4441
+ return {
4442
+ embeddings,
4443
+ ...typeof json.model === "string" && json.model ? { model: json.model } : {},
4444
+ ...typeof promptTokens === "number" && Number.isFinite(promptTokens) ? { usage: { inputTokens: promptTokens } } : {}
4445
+ };
4446
+ }
4447
+ async function rerankCohereCompatible(request, config) {
4448
+ const body = mergeProviderOptions(
4449
+ {
4450
+ model: config.wireModel ?? request.model,
4451
+ query: request.query,
4452
+ documents: request.documents,
4453
+ ...request.topN !== void 0 ? { top_n: request.topN } : {}
4454
+ },
4455
+ request.providerOptions
4456
+ );
4457
+ const { json, requestId } = await postJson({
4458
+ provider: config.provider,
4459
+ url: joinUrl(config.baseUrl, "rerank"),
4460
+ headers: jsonHeaders(request.apiKey),
4461
+ body,
4462
+ fetch: request.fetch,
4463
+ signal: request.signal,
4464
+ secrets: [request.apiKey]
4465
+ });
4466
+ const invalid = (detail) => new ProviderError(config.provider, `Invalid rerank response: ${detail}.`, {
4467
+ ...requestId ? { requestId } : {}
4468
+ });
4469
+ if (!isRecord(json) || !Array.isArray(json.results)) throw invalid("missing `results` array");
4470
+ const results = json.results.map((item) => {
4471
+ if (!isRecord(item)) throw invalid("an entry in `results` is not an object");
4472
+ const { index, relevance_score: score } = item;
4473
+ if (typeof index !== "number" || typeof score !== "number") {
4474
+ throw invalid("an entry is missing a numeric `index` or `relevance_score`");
4475
+ }
4476
+ return { index, score };
4477
+ });
4478
+ const usage = json.usage;
4479
+ const tokens = isRecord(usage) ? usage.prompt_tokens ?? usage.total_tokens : void 0;
4480
+ const searchUnits = isRecord(usage) ? usage.search_units : void 0;
4481
+ const rerankUsage = {
4482
+ ...typeof tokens === "number" && Number.isFinite(tokens) ? { inputTokens: tokens } : {},
4483
+ ...typeof searchUnits === "number" && Number.isFinite(searchUnits) ? { searchUnits } : {}
4484
+ };
4485
+ return {
4486
+ results,
4487
+ ...typeof json.model === "string" && json.model ? { model: json.model } : {},
4488
+ ...Object.keys(rerankUsage).length > 0 ? { usage: rerankUsage } : {}
4489
+ };
4490
+ }
4491
+
3991
4492
  // src/provider-registry.ts
3992
4493
  var ProviderRegistryImpl = class {
3993
4494
  providers = /* @__PURE__ */ new Map();
@@ -4017,6 +4518,11 @@ var ProviderRegistryImpl = class {
4017
4518
  has(name) {
4018
4519
  return this.providers.has(name);
4019
4520
  }
4521
+ /** Whether a registered provider offers a capability (`false` for unknown providers). */
4522
+ supports(name, capability) {
4523
+ const entry = this.providers.get(name);
4524
+ return typeof entry?.[capability] === "function";
4525
+ }
4020
4526
  /** List all registered provider names. */
4021
4527
  list() {
4022
4528
  return [...this.providers.keys()];
@@ -4194,6 +4700,10 @@ function observePreparedContext(before, after, tools) {
4194
4700
  }
4195
4701
 
4196
4702
  // src/stream.ts
4703
+ var OPENAI_EMBED_BATCH_SIZE = 2048;
4704
+ var OPENROUTER_EMBED_BATCH_SIZE = 64;
4705
+ var LOCAL_EMBED_BATCH_SIZE = 64;
4706
+ var OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
4197
4707
  var GLM_CODING_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
4198
4708
  var KIMI_CODE_USER_AGENT = `kimi-code-cli/${process.env.KIMI_CODE_VERSION ?? "1.0.11"}`;
4199
4709
  var GROK_CLI_PROXY_HOST = "cli-chat-proxy.grok.com";
@@ -4214,10 +4724,20 @@ providerRegistry.register("openai", {
4214
4724
  return streamOpenAICodex(options);
4215
4725
  }
4216
4726
  return streamOpenAI(options);
4217
- }
4727
+ },
4728
+ // OpenAI API-key auth only; ChatGPT-subscription (Codex) tokens have no embeddings API.
4729
+ // OpenAI has no task/input-type parameter, so `inputType` is ignored.
4730
+ embed: (request) => embedOpenAICompatible(request, {
4731
+ provider: "openai",
4732
+ baseUrl: request.baseUrl ?? "https://api.openai.com/v1"
4733
+ }),
4734
+ embedBatchSize: OPENAI_EMBED_BATCH_SIZE
4218
4735
  });
4219
4736
  providerRegistry.register("gemini", {
4220
- stream: (options) => streamGemini(options)
4737
+ stream: (options) => streamGemini(options),
4738
+ // Native Gemini API with an AI Studio key — not the Code Assist OAuth endpoint above.
4739
+ embed: (request) => embedGemini(request),
4740
+ embedBatchSize: GEMINI_EMBED_BATCH_SIZE
4221
4741
  });
4222
4742
  providerRegistry.register("glm", {
4223
4743
  stream: (options) => streamOpenAI({
@@ -4241,7 +4761,19 @@ providerRegistry.register("deepseek", {
4241
4761
  providerRegistry.register("openrouter", {
4242
4762
  stream: (options) => streamOpenAI({
4243
4763
  ...options,
4244
- baseUrl: options.baseUrl ?? "https://openrouter.ai/api/v1"
4764
+ baseUrl: options.baseUrl ?? OPENROUTER_BASE_URL
4765
+ }),
4766
+ embed: (request) => embedOpenAICompatible(request, {
4767
+ provider: "openrouter",
4768
+ baseUrl: request.baseUrl ?? OPENROUTER_BASE_URL,
4769
+ inputTypeFields: (inputType) => ({
4770
+ input_type: inputType === "query" ? "search_query" : "search_document"
4771
+ })
4772
+ }),
4773
+ embedBatchSize: OPENROUTER_EMBED_BATCH_SIZE,
4774
+ rerank: (request) => rerankCohereCompatible(request, {
4775
+ provider: "openrouter",
4776
+ baseUrl: request.baseUrl ?? OPENROUTER_BASE_URL
4245
4777
  })
4246
4778
  });
4247
4779
  providerRegistry.register("huggingface", {
@@ -4311,19 +4843,36 @@ providerRegistry.register("local", {
4311
4843
  // vLLM). There is no default endpoint: the baseUrl comes from the endpoint
4312
4844
  // credential the discovery layer wrote, so a missing one is a wiring bug, not
4313
4845
  // something to paper over with a guess at someone else's port.
4314
- stream: (options) => {
4315
- if (!options.baseUrl) {
4316
- throw new GGAIError(
4317
- "Local provider requires a baseUrl (e.g. http://127.0.0.1:11434/v1). No local endpoint was resolved for this model \u2014 re-scan for local models."
4318
- );
4319
- }
4320
- return streamOpenAI({
4321
- ...options,
4322
- model: localWireModelId(options.model),
4323
- webSearch: false
4324
- });
4325
- }
4846
+ stream: (options) => streamOpenAI({
4847
+ ...options,
4848
+ baseUrl: requireLocalBaseUrl(options.baseUrl),
4849
+ model: localWireModelId(options.model),
4850
+ webSearch: false
4851
+ }),
4852
+ // Every supported local server exposes OpenAI-compatible `/v1/embeddings`
4853
+ // (the model must be an embedding model). None has an input-type parameter.
4854
+ embed: (request) => embedOpenAICompatible(request, {
4855
+ provider: "local",
4856
+ baseUrl: requireLocalBaseUrl(request.baseUrl),
4857
+ wireModel: localWireModelId(request.model)
4858
+ }),
4859
+ embedBatchSize: LOCAL_EMBED_BATCH_SIZE,
4860
+ // llama.cpp (`llama-server --rerank`) and vLLM serve `/v1/rerank`; Ollama and
4861
+ // LM Studio don't, and answer with a 404 ProviderError.
4862
+ rerank: (request) => rerankCohereCompatible(request, {
4863
+ provider: "local",
4864
+ baseUrl: requireLocalBaseUrl(request.baseUrl),
4865
+ wireModel: localWireModelId(request.model)
4866
+ })
4326
4867
  });
4868
+ function requireLocalBaseUrl(baseUrl) {
4869
+ if (!baseUrl) {
4870
+ throw new GGAIError(
4871
+ "Local provider requires a baseUrl (e.g. http://127.0.0.1:11434/v1). No local endpoint was resolved for this model \u2014 re-scan for local models."
4872
+ );
4873
+ }
4874
+ return baseUrl;
4875
+ }
4327
4876
  function stream(options) {
4328
4877
  const entry = providerRegistry.get(options.provider);
4329
4878
  if (!entry) {
@@ -4382,6 +4931,121 @@ function messagesContainVideo(messages) {
4382
4931
  return false;
4383
4932
  }
4384
4933
 
4934
+ // src/embed.ts
4935
+ var CAPABILITY_LABEL = {
4936
+ embed: "embeddings",
4937
+ rerank: "reranking"
4938
+ };
4939
+ function resolveCapability(provider, capability) {
4940
+ const entry = providerRegistry.get(provider);
4941
+ if (!entry) {
4942
+ throw new GGAIError(
4943
+ `Unknown provider: "${provider}". Registered: ${providerRegistry.list().join(", ")}`
4944
+ );
4945
+ }
4946
+ const fn = entry[capability];
4947
+ if (typeof fn !== "function") {
4948
+ const label = CAPABILITY_LABEL[capability];
4949
+ const offering = providerRegistry.list().filter((name) => providerRegistry.supports(name, capability)).sort();
4950
+ throw new GGAIError(`Provider "${provider}" doesn't support ${label}.`, {
4951
+ source: "capability",
4952
+ hint: offering.length > 0 ? `Use a provider that offers ${label}: ${offering.join(", ")}.` : `No registered provider offers ${label}.`
4953
+ });
4954
+ }
4955
+ return fn;
4956
+ }
4957
+ function assertPositiveInteger(name, value) {
4958
+ if (value !== void 0 && !(Number.isInteger(value) && value > 0)) {
4959
+ throw new GGAIError(`${name} must be a positive integer (got ${String(value)}).`);
4960
+ }
4961
+ }
4962
+ async function embed(options) {
4963
+ const embedFn = resolveCapability(options.provider, "embed");
4964
+ if (!Array.isArray(options.input)) {
4965
+ throw new GGAIError("embed() input must be an array of strings.");
4966
+ }
4967
+ assertPositiveInteger("dimensions", options.dimensions);
4968
+ assertPositiveInteger("batchSize", options.batchSize);
4969
+ if (options.input.length === 0) return { embeddings: [], model: options.model };
4970
+ const entry = providerRegistry.get(options.provider);
4971
+ const batchSize = options.batchSize ?? entry?.embedBatchSize ?? options.input.length;
4972
+ const { normalize, batchSize: _batchSize, ...request } = options;
4973
+ const embeddings = [];
4974
+ let model = options.model;
4975
+ let inputTokens;
4976
+ let expectedLength = options.dimensions;
4977
+ for (let start = 0; start < options.input.length; start += batchSize) {
4978
+ throwIfAborted(options.signal);
4979
+ const batch = options.input.slice(start, start + batchSize);
4980
+ const response = await embedFn({ ...request, input: batch });
4981
+ const invalid = (detail) => new ProviderError(options.provider, `Invalid embeddings response: ${detail}.`);
4982
+ if (!Array.isArray(response.embeddings) || response.embeddings.length !== batch.length) {
4983
+ const count = Array.isArray(response.embeddings) ? response.embeddings.length : 0;
4984
+ throw invalid(`expected ${batch.length} embeddings, got ${count}`);
4985
+ }
4986
+ for (const vector of response.embeddings) {
4987
+ if (!Array.isArray(vector) || vector.length === 0) throw invalid("received an empty vector");
4988
+ if (!vector.every((value) => typeof value === "number" && Number.isFinite(value))) {
4989
+ throw invalid("received a vector with non-numeric values");
4990
+ }
4991
+ if (expectedLength === void 0) {
4992
+ expectedLength = vector.length;
4993
+ } else if (vector.length !== expectedLength) {
4994
+ throw invalid(
4995
+ options.dimensions !== void 0 ? `requested ${options.dimensions} dimensions, got a vector with ${vector.length}` : `vectors have different lengths (${expectedLength} and ${vector.length})`
4996
+ );
4997
+ }
4998
+ embeddings.push(normalize ? l2Normalize(vector) : vector);
4999
+ }
5000
+ if (response.model) model = response.model;
5001
+ if (response.usage) inputTokens = (inputTokens ?? 0) + response.usage.inputTokens;
5002
+ }
5003
+ return {
5004
+ embeddings,
5005
+ model,
5006
+ ...inputTokens !== void 0 ? { usage: { inputTokens } } : {}
5007
+ };
5008
+ }
5009
+ async function rerank(options) {
5010
+ const rerankFn = resolveCapability(options.provider, "rerank");
5011
+ if (!Array.isArray(options.documents)) {
5012
+ throw new GGAIError("rerank() documents must be an array of strings.");
5013
+ }
5014
+ assertPositiveInteger("topN", options.topN);
5015
+ if (options.documents.length === 0) return { results: [], model: options.model };
5016
+ throwIfAborted(options.signal);
5017
+ const response = await rerankFn(options);
5018
+ const invalid = (detail) => new ProviderError(options.provider, `Invalid rerank response: ${detail}.`);
5019
+ if (!Array.isArray(response.results)) throw invalid("missing results");
5020
+ const seen = /* @__PURE__ */ new Set();
5021
+ const results = response.results.map(({ index, score }) => {
5022
+ if (!Number.isInteger(index) || index < 0 || index >= options.documents.length) {
5023
+ throw invalid(
5024
+ `index ${String(index)} is out of range for ${options.documents.length} documents`
5025
+ );
5026
+ }
5027
+ if (seen.has(index)) throw invalid(`index ${index} appears more than once`);
5028
+ if (typeof score !== "number" || !Number.isFinite(score)) {
5029
+ throw invalid(`document ${index} has a non-numeric score`);
5030
+ }
5031
+ seen.add(index);
5032
+ return { index, score };
5033
+ });
5034
+ results.sort((a, b) => b.score - a.score || a.index - b.index);
5035
+ return {
5036
+ results: options.topN !== void 0 ? results.slice(0, options.topN) : results,
5037
+ model: response.model ?? options.model,
5038
+ ...response.usage ? { usage: response.usage } : {}
5039
+ };
5040
+ }
5041
+ function l2Normalize(vector) {
5042
+ let sumOfSquares = 0;
5043
+ for (const value of vector) sumOfSquares += value * value;
5044
+ if (sumOfSquares === 0) return [...vector];
5045
+ const norm = Math.sqrt(sumOfSquares);
5046
+ return vector.map((value) => value / norm);
5047
+ }
5048
+
4385
5049
  // src/chat-error.ts
4386
5050
  function causeCode(error) {
4387
5051
  if (!(error instanceof Error) || !error.cause || typeof error.cause !== "object") return "";
@@ -4623,143 +5287,6 @@ Original: ${message}`;
4623
5287
  return message;
4624
5288
  }
4625
5289
 
4626
- // src/redaction.ts
4627
- var REDACTED = "[REDACTED]";
4628
- var TRUNCATED = "[TRUNCATED]";
4629
- var CIRCULAR = "[CIRCULAR]";
4630
- var SENSITIVE_NAME = /(?:^|[_-])(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|key|auth(?:orization)?|bearer|cookie|credential|private[_-]?key|password|passwd|secret)(?:$|[_-])/i;
4631
- var ENV_SECRET_ASSIGNMENT = /\b((?:[A-Z0-9]+_)*(?:API_?KEY|ACCESS_TOKEN|REFRESH_TOKEN|TOKEN|KEY|AUTH|AUTHORIZATION|BEARER|CREDENTIALS?|PASSWORD|PASSWD|SECRET))\b(\s*[=:]\s*)(["']?)(?!\$|process\.env|os\.environ|import\.meta|env\.)(?=[^\s,"';}]*\d)([^\s,"';}=$][^\s,"';}]{7,})\3/g;
4632
- var COMPACT_SECRET_ASSIGNMENT = /\b((?:[a-z0-9]+[_-])*(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|auth|authorization|credentials?|password|passwd|secret)|(?:[a-z0-9]+[_-])+key)=(["']?)(?!\$)([^\s,"'&;}=][^\s,"'&;}]{7,})\2/gi;
4633
- var URL_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#@:"'`<>]*:[^\s/?#"'`<>]+@/gi;
4634
- var URL_USERINFO_TO_FIRST_AT = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]*:[^\s/@]+@/gi;
4635
- function escaped(value) {
4636
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4637
- }
4638
- function normalizedSecrets(secrets) {
4639
- if (!secrets) return [];
4640
- return [...new Set([...secrets].filter((value) => value.length >= 8 && value !== REDACTED))].sort(
4641
- (a, b) => b.length - a.length
4642
- );
4643
- }
4644
- function environmentSecrets(env) {
4645
- const values = /* @__PURE__ */ new Set();
4646
- for (const [name, value] of Object.entries(env)) {
4647
- if (!value || value.length < 8 || value === REDACTED || !SENSITIVE_NAME.test(name)) continue;
4648
- values.add(value);
4649
- }
4650
- return [...values].sort((a, b) => b.length - a.length);
4651
- }
4652
- function redactText(text, options = {}) {
4653
- let result = text;
4654
- result = result.replace(
4655
- /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g,
4656
- REDACTED
4657
- );
4658
- result = result.replace(URL_USERINFO, `$1${REDACTED}@`);
4659
- result = result.replace(URL_USERINFO_TO_FIRST_AT, `$1${REDACTED}@`);
4660
- result = result.replace(
4661
- /\b(authorization\s*[:=]\s*)(?:bearer|basic)\s+[^\s,;]+/gi,
4662
- `$1${REDACTED}`
4663
- );
4664
- result = result.replace(
4665
- /\b(bearer|basic)\s+(?![A-Za-z][a-z]{0,14}\b)[A-Za-z0-9+/_.=-]{8,}/gi,
4666
- `$1 ${REDACTED}`
4667
- );
4668
- result = result.replace(/\b(cookie|set-cookie)(\s*:\s*)[^\s=;:]+=[^\r\n]*/gi, `$1$2${REDACTED}`);
4669
- result = result.replace(
4670
- /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g,
4671
- REDACTED
4672
- );
4673
- result = result.replace(
4674
- /\b(?:sk-(?:ant-|proj-)?|xox[baprs]-|gh[pousr]_|github_pat_|AIza)[A-Za-z0-9_-]{12,}\b/g,
4675
- REDACTED
4676
- );
4677
- result = result.replace(
4678
- ENV_SECRET_ASSIGNMENT,
4679
- (_match, name, separator, quote) => `${name}${separator}${quote}${REDACTED}${quote}`
4680
- );
4681
- result = result.replace(
4682
- COMPACT_SECRET_ASSIGNMENT,
4683
- (_match, name, quote) => `${name}=${quote}${REDACTED}${quote}`
4684
- );
4685
- for (const secret of normalizedSecrets(options.secrets)) {
4686
- result = result.replace(new RegExp(escaped(secret), "g"), REDACTED);
4687
- }
4688
- const maxStringLength = options.maxStringLength ?? 1e6;
4689
- if (result.length > maxStringLength) {
4690
- result = `${result.slice(0, maxStringLength)}${TRUNCATED}`;
4691
- }
4692
- return result;
4693
- }
4694
- function isBinary(value) {
4695
- return value instanceof ArrayBuffer || ArrayBuffer.isView(value) || typeof Blob !== "undefined" && value instanceof Blob;
4696
- }
4697
- function isMediaObject(value) {
4698
- return (value.type === "image" || value.type === "video") && (typeof value.data === "string" || typeof value.url === "string");
4699
- }
4700
- function redactValue(value, options = {}) {
4701
- const maxDepth = options.maxDepth ?? 20;
4702
- const maxEntries = options.maxEntries ?? 1e4;
4703
- const ancestors = /* @__PURE__ */ new WeakSet();
4704
- let entries = 0;
4705
- const visit = (current, depth, sensitive = false) => {
4706
- if (typeof current === "string") {
4707
- if (sensitive && current.length > 0 && current !== REDACTED) return REDACTED;
4708
- return redactText(current, options);
4709
- }
4710
- if (current === null || current === void 0 || typeof current === "number" || typeof current === "boolean" || typeof current === "bigint") {
4711
- return current;
4712
- }
4713
- if (typeof current !== "object") return current;
4714
- if (isBinary(current)) return current;
4715
- if (current instanceof Date) return new Date(current.getTime());
4716
- if (depth >= maxDepth) return TRUNCATED;
4717
- if (ancestors.has(current)) return CIRCULAR;
4718
- ancestors.add(current);
4719
- try {
4720
- return cloneObject(current, depth);
4721
- } finally {
4722
- ancestors.delete(current);
4723
- }
4724
- };
4725
- const cloneObject = (current, depth) => {
4726
- if (current instanceof Error) {
4727
- const error = {
4728
- name: current.name,
4729
- message: visit(current.message, depth + 1),
4730
- stack: visit(current.stack, depth + 1)
4731
- };
4732
- for (const [key, child] of Object.entries(current)) {
4733
- error[key] = visit(child, depth + 1, SENSITIVE_NAME.test(key));
4734
- }
4735
- return error;
4736
- }
4737
- if (Array.isArray(current)) {
4738
- const clone2 = [];
4739
- for (const child of current) {
4740
- if (++entries > maxEntries) {
4741
- clone2.push(TRUNCATED);
4742
- break;
4743
- }
4744
- clone2.push(visit(child, depth + 1));
4745
- }
4746
- return clone2;
4747
- }
4748
- const record = current;
4749
- if (isMediaObject(record)) return { ...record };
4750
- const clone = {};
4751
- for (const [key, child] of Object.entries(record)) {
4752
- if (++entries > maxEntries) {
4753
- clone[TRUNCATED] = true;
4754
- break;
4755
- }
4756
- clone[key] = visit(child, depth + 1, SENSITIVE_NAME.test(key));
4757
- }
4758
- return clone;
4759
- };
4760
- return visit(value, 0);
4761
- }
4762
-
4763
5290
  // src/providers/palsu.ts
4764
5291
  function palsuText(text) {
4765
5292
  return { role: "assistant", content: text ? [{ type: "text", text }] : [] };
@@ -4840,7 +5367,8 @@ function computeCacheUsage(current, previous) {
4840
5367
  function registerPalsuProvider(config) {
4841
5368
  const name = config?.name ?? "palsu";
4842
5369
  const responses = [];
4843
- const state = { callCount: 0 };
5370
+ const state = { callCount: 0, embedCallCount: 0, rerankCallCount: 0 };
5371
+ const embedDimensions = config?.embedDimensions ?? PALSU_DEFAULT_EMBED_DIMENSIONS;
4844
5372
  const defaultResponse = config?.defaultResponse ?? palsuText("");
4845
5373
  const enableCache = config?.promptCache ?? false;
4846
5374
  let lastMessagesSerialized = null;
@@ -4908,10 +5436,72 @@ function registerPalsuProvider(config) {
4908
5436
  return yield* simulateStream(message, stopReason, options.signal, cacheUsage);
4909
5437
  })();
4910
5438
  return new StreamResult(gen, options.signal);
5439
+ },
5440
+ async embed(request) {
5441
+ throwIfAborted(request.signal);
5442
+ state.embedCallCount++;
5443
+ const dimensions = request.dimensions ?? embedDimensions;
5444
+ return {
5445
+ embeddings: request.input.map((text) => palsuEmbedding(text, dimensions)),
5446
+ model: request.model,
5447
+ usage: { inputTokens: request.input.reduce((n, text) => n + tokenize(text).length, 0) }
5448
+ };
5449
+ },
5450
+ ...config?.embedBatchSize !== void 0 ? { embedBatchSize: config.embedBatchSize } : {},
5451
+ async rerank(request) {
5452
+ throwIfAborted(request.signal);
5453
+ state.rerankCallCount++;
5454
+ return {
5455
+ results: request.documents.map((document, index) => ({
5456
+ index,
5457
+ score: palsuRerankScore(request.query, document)
5458
+ })),
5459
+ model: request.model
5460
+ };
4911
5461
  }
4912
5462
  });
4913
5463
  return handle;
4914
5464
  }
5465
+ var PALSU_DEFAULT_EMBED_DIMENSIONS = 64;
5466
+ function tokenize(text) {
5467
+ return text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
5468
+ }
5469
+ function fnv1a(text) {
5470
+ let hash = 2166136261;
5471
+ for (let i = 0; i < text.length; i++) {
5472
+ hash ^= text.charCodeAt(i);
5473
+ hash = Math.imul(hash, 16777619);
5474
+ }
5475
+ return hash >>> 0;
5476
+ }
5477
+ function palsuEmbedding(text, dimensions = PALSU_DEFAULT_EMBED_DIMENSIONS) {
5478
+ const vector = new Array(dimensions).fill(0);
5479
+ const tokens = tokenize(text);
5480
+ if (tokens.length === 0) {
5481
+ vector[0] = 1;
5482
+ return vector;
5483
+ }
5484
+ for (const token of tokens) {
5485
+ const hash = fnv1a(token);
5486
+ const slot = hash % dimensions;
5487
+ vector[slot] = (vector[slot] ?? 0) + (hash >>> 31 === 0 ? 1 : -1);
5488
+ }
5489
+ const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
5490
+ if (norm === 0) {
5491
+ vector.fill(0);
5492
+ vector[0] = 1;
5493
+ return vector;
5494
+ }
5495
+ return vector.map((value) => value / norm);
5496
+ }
5497
+ function palsuRerankScore(query, document) {
5498
+ const queryTokens = new Set(tokenize(query));
5499
+ if (queryTokens.size === 0) return 0;
5500
+ const documentTokens = new Set(tokenize(document));
5501
+ let hits = 0;
5502
+ for (const token of queryTokens) if (documentTokens.has(token)) hits++;
5503
+ return hits / queryTokens.size;
5504
+ }
4915
5505
  export {
4916
5506
  EventStream,
4917
5507
  GGAIError,
@@ -4922,6 +5512,7 @@ export {
4922
5512
  clampProviderContextImages,
4923
5513
  classifyProviderError,
4924
5514
  dropInvalidToolCalls,
5515
+ embed,
4925
5516
  environmentSecrets,
4926
5517
  formatChatError,
4927
5518
  formatError,
@@ -4932,6 +5523,8 @@ export {
4932
5523
  isValidToolCallName,
4933
5524
  localWireModelId,
4934
5525
  palsuAssistantMessage,
5526
+ palsuEmbedding,
5527
+ palsuRerankScore,
4935
5528
  palsuText,
4936
5529
  palsuThinking,
4937
5530
  palsuToolCall,
@@ -4940,6 +5533,7 @@ export {
4940
5533
  redactText,
4941
5534
  redactValue,
4942
5535
  registerPalsuProvider,
5536
+ rerank,
4943
5537
  resolveToolSchema,
4944
5538
  sanitizeMessagesForWire,
4945
5539
  setProviderDiagnostic,