@kenkaiiii/gg-ai 5.75.1 → 5.77.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.cjs CHANGED
@@ -39,6 +39,7 @@ __export(index_exports, {
39
39
  clampProviderContextImages: () => clampProviderContextImages,
40
40
  classifyProviderError: () => classifyProviderError,
41
41
  dropInvalidToolCalls: () => dropInvalidToolCalls,
42
+ embed: () => embed,
42
43
  environmentSecrets: () => environmentSecrets,
43
44
  formatChatError: () => formatChatError,
44
45
  formatError: () => formatError,
@@ -49,6 +50,8 @@ __export(index_exports, {
49
50
  isValidToolCallName: () => isValidToolCallName,
50
51
  localWireModelId: () => localWireModelId,
51
52
  palsuAssistantMessage: () => palsuAssistantMessage,
53
+ palsuEmbedding: () => palsuEmbedding,
54
+ palsuRerankScore: () => palsuRerankScore,
52
55
  palsuText: () => palsuText,
53
56
  palsuThinking: () => palsuThinking,
54
57
  palsuToolCall: () => palsuToolCall,
@@ -57,6 +60,7 @@ __export(index_exports, {
57
60
  redactText: () => redactText,
58
61
  redactValue: () => redactValue,
59
62
  registerPalsuProvider: () => registerPalsuProvider,
63
+ rerank: () => rerank,
60
64
  resolveToolSchema: () => resolveToolSchema,
61
65
  sanitizeMessagesForWire: () => sanitizeMessagesForWire,
62
66
  setProviderDiagnostic: () => setProviderDiagnostic,
@@ -111,7 +115,7 @@ var ProviderError = class extends GGAIError {
111
115
  resetsAt;
112
116
  constructor(provider, message, options) {
113
117
  super(message, {
114
- source: "provider",
118
+ source: options?.source ?? "provider",
115
119
  requestId: options?.requestId,
116
120
  hint: options?.hint,
117
121
  cause: options?.cause
@@ -508,6 +512,46 @@ var StreamResult = class _StreamResult {
508
512
  }
509
513
  };
510
514
 
515
+ // src/utils/provider-options.ts
516
+ var RESERVED_BODY_FIELDS = /* @__PURE__ */ new Set([
517
+ "contents",
518
+ "dimensions",
519
+ "documents",
520
+ "encoding_format",
521
+ "input",
522
+ "messages",
523
+ "model",
524
+ "query",
525
+ "requests",
526
+ "stream",
527
+ "system",
528
+ "tools",
529
+ "top_n",
530
+ // Never copy prototype-shaped keys onto a request body.
531
+ "__proto__",
532
+ "constructor",
533
+ "prototype"
534
+ ]);
535
+ function mergeProviderOptions(body, providerOptions) {
536
+ if (!providerOptions) return body;
537
+ const base = body;
538
+ const extra = {};
539
+ for (const key of Object.keys(providerOptions).sort()) {
540
+ if (RESERVED_BODY_FIELDS.has(key)) continue;
541
+ if (Object.hasOwn(base, key) && base[key] !== void 0) continue;
542
+ const value = providerOptions[key];
543
+ if (value === void 0) continue;
544
+ Object.defineProperty(extra, key, {
545
+ value,
546
+ enumerable: true,
547
+ writable: true,
548
+ configurable: true
549
+ });
550
+ }
551
+ if (Object.keys(extra).length === 0) return body;
552
+ return { ...base, ...extra };
553
+ }
554
+
511
555
  // src/utils/zod-to-json-schema.ts
512
556
  var import_zod = require("zod");
513
557
  var schemaCache = /* @__PURE__ */ new WeakMap();
@@ -1750,7 +1794,7 @@ async function* runStream(options) {
1750
1794
  }
1751
1795
  maxTokens = 1;
1752
1796
  }
1753
- const params = {
1797
+ const baseParams = {
1754
1798
  model: options.model,
1755
1799
  max_tokens: maxTokens,
1756
1800
  messages,
@@ -1793,6 +1837,7 @@ async function* runStream(options) {
1793
1837
  ...useServerFallback ? { fallbacks: "default" } : {},
1794
1838
  stream: useStreaming
1795
1839
  };
1840
+ const params = mergeProviderOptions(baseParams, options.providerOptions);
1796
1841
  const hasAdaptiveThinking = isAdaptiveThinkingModel(options.model);
1797
1842
  const betaHeaders = [
1798
1843
  ...isOAuth ? ["claude-code-20250219", "oauth-2025-04-20"] : [],
@@ -2498,20 +2543,21 @@ async function* runStream2(options) {
2498
2543
  params.thinking = { type: "disabled" };
2499
2544
  }
2500
2545
  }
2546
+ const requestParams = mergeProviderOptions(params, options.providerOptions);
2501
2547
  if (getEnvironment()?.GGAI_DUMP_REQUEST) {
2502
2548
  const fs = await import("fs");
2503
2549
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2504
2550
  const dumpPath = `/tmp/ggai-request-${ts}.json`;
2505
- fs.writeFileSync(dumpPath, JSON.stringify(params, null, 2));
2551
+ fs.writeFileSync(dumpPath, JSON.stringify(requestParams, null, 2));
2506
2552
  fs.appendFileSync(
2507
2553
  "/tmp/ggai-requests.log",
2508
- `[${ts}] ${dumpPath} messages=${params.messages.length}
2554
+ `[${ts}] ${dumpPath} messages=${requestParams.messages.length}
2509
2555
  `
2510
2556
  );
2511
2557
  }
2512
2558
  if (!useStreaming) {
2513
2559
  try {
2514
- const completion = await client.chat.completions.create(params, {
2560
+ const completion = await client.chat.completions.create(requestParams, {
2515
2561
  signal: options.signal ?? void 0
2516
2562
  });
2517
2563
  yield* synthesizeEventsFromCompletion(completion, !!options.thinking, endpointKey);
@@ -2522,7 +2568,7 @@ async function* runStream2(options) {
2522
2568
  }
2523
2569
  let stream2;
2524
2570
  try {
2525
- stream2 = await client.chat.completions.create(params, {
2571
+ stream2 = await client.chat.completions.create(requestParams, {
2526
2572
  signal: options.signal ?? void 0
2527
2573
  });
2528
2574
  } catch (err) {
@@ -3010,7 +3056,9 @@ async function* runStream3(options, retriedWithoutReasoning = false) {
3010
3056
  headers["session_id"] = transportSessionId;
3011
3057
  headers["x-client-request-id"] = transportSessionId;
3012
3058
  }
3013
- const encodedRequest = await encodeCodexRequest(body);
3059
+ const encodedRequest = await encodeCodexRequest(
3060
+ mergeProviderOptions(body, options.providerOptions)
3061
+ );
3014
3062
  if (encodedRequest.compressed) headers["Content-Encoding"] = "zstd";
3015
3063
  providerDiag("codex_request_body", {
3016
3064
  rawBytes: encodedRequest.rawBytes,
@@ -3725,11 +3773,11 @@ function stripUnsupportedSchemaFields(value) {
3725
3773
  ["exclusiveMinimum", "minimum", 1],
3726
3774
  ["exclusiveMaximum", "maximum", -1]
3727
3775
  ]) {
3728
- const bound = value[key];
3729
- if (bound === void 0) continue;
3776
+ const bound2 = value[key];
3777
+ if (bound2 === void 0) continue;
3730
3778
  delete value[key];
3731
- if (typeof bound === "number" && value[target] === void 0) {
3732
- value[target] = value.type === "integer" ? bound + step : bound;
3779
+ if (typeof bound2 === "number" && value[target] === void 0) {
3780
+ value[target] = value.type === "integer" ? bound2 + step : bound2;
3733
3781
  }
3734
3782
  }
3735
3783
  for (const item of Object.values(value)) {
@@ -3823,7 +3871,7 @@ function buildRequestPlan(options, method) {
3823
3871
  throw new ProviderError("gemini", formatUnsupportedModelMessage(options.model));
3824
3872
  }
3825
3873
  const projectId = getGoogleProject(options);
3826
- const request = buildGenerateRequest(options);
3874
+ const request = mergeProviderOptions(buildGenerateRequest(options), options.providerOptions);
3827
3875
  return {
3828
3876
  url: getCodeAssistEndpoint(method),
3829
3877
  headers: {
@@ -4061,6 +4109,449 @@ function toError3(err) {
4061
4109
  return new ProviderError("gemini", String(err));
4062
4110
  }
4063
4111
 
4112
+ // src/redaction.ts
4113
+ var REDACTED = "[REDACTED]";
4114
+ var TRUNCATED = "[TRUNCATED]";
4115
+ var CIRCULAR = "[CIRCULAR]";
4116
+ var SENSITIVE_NAME = /(?:^|[_-])(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|key|auth(?:orization)?|bearer|cookie|credential|private[_-]?key|password|passwd|secret)(?:$|[_-])/i;
4117
+ 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;
4118
+ 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;
4119
+ var URL_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#@:"'`<>]*:[^\s/?#"'`<>]+@/gi;
4120
+ var URL_USERINFO_TO_FIRST_AT = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]*:[^\s/@]+@/gi;
4121
+ function escaped(value) {
4122
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4123
+ }
4124
+ function normalizedSecrets(secrets) {
4125
+ if (!secrets) return [];
4126
+ return [...new Set([...secrets].filter((value) => value.length >= 8 && value !== REDACTED))].sort(
4127
+ (a, b) => b.length - a.length
4128
+ );
4129
+ }
4130
+ function environmentSecrets(env) {
4131
+ const values = /* @__PURE__ */ new Set();
4132
+ for (const [name, value] of Object.entries(env)) {
4133
+ if (!value || value.length < 8 || value === REDACTED || !SENSITIVE_NAME.test(name)) continue;
4134
+ values.add(value);
4135
+ }
4136
+ return [...values].sort((a, b) => b.length - a.length);
4137
+ }
4138
+ function redactText(text, options = {}) {
4139
+ let result = text;
4140
+ result = result.replace(
4141
+ /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g,
4142
+ REDACTED
4143
+ );
4144
+ result = result.replace(URL_USERINFO, `$1${REDACTED}@`);
4145
+ result = result.replace(URL_USERINFO_TO_FIRST_AT, `$1${REDACTED}@`);
4146
+ result = result.replace(
4147
+ /\b(authorization\s*[:=]\s*)(?:bearer|basic)\s+[^\s,;]+/gi,
4148
+ `$1${REDACTED}`
4149
+ );
4150
+ result = result.replace(
4151
+ /\b(bearer|basic)\s+(?![A-Za-z][a-z]{0,14}\b)[A-Za-z0-9+/_.=-]{8,}/gi,
4152
+ `$1 ${REDACTED}`
4153
+ );
4154
+ result = result.replace(/\b(cookie|set-cookie)(\s*:\s*)[^\s=;:]+=[^\r\n]*/gi, `$1$2${REDACTED}`);
4155
+ result = result.replace(
4156
+ /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g,
4157
+ REDACTED
4158
+ );
4159
+ result = result.replace(
4160
+ /\b(?:sk-(?:ant-|proj-)?|xox[baprs]-|gh[pousr]_|github_pat_|AIza)[A-Za-z0-9_-]{12,}\b/g,
4161
+ REDACTED
4162
+ );
4163
+ result = result.replace(
4164
+ ENV_SECRET_ASSIGNMENT,
4165
+ (_match, name, separator, quote) => `${name}${separator}${quote}${REDACTED}${quote}`
4166
+ );
4167
+ result = result.replace(
4168
+ COMPACT_SECRET_ASSIGNMENT,
4169
+ (_match, name, quote) => `${name}=${quote}${REDACTED}${quote}`
4170
+ );
4171
+ for (const secret of normalizedSecrets(options.secrets)) {
4172
+ result = result.replace(new RegExp(escaped(secret), "g"), REDACTED);
4173
+ }
4174
+ const maxStringLength = options.maxStringLength ?? 1e6;
4175
+ if (result.length > maxStringLength) {
4176
+ result = `${result.slice(0, maxStringLength)}${TRUNCATED}`;
4177
+ }
4178
+ return result;
4179
+ }
4180
+ function isBinary(value) {
4181
+ return value instanceof ArrayBuffer || ArrayBuffer.isView(value) || typeof Blob !== "undefined" && value instanceof Blob;
4182
+ }
4183
+ function isMediaObject(value) {
4184
+ return (value.type === "image" || value.type === "video") && (typeof value.data === "string" || typeof value.url === "string");
4185
+ }
4186
+ function redactValue(value, options = {}) {
4187
+ const maxDepth = options.maxDepth ?? 20;
4188
+ const maxEntries = options.maxEntries ?? 1e4;
4189
+ const ancestors = /* @__PURE__ */ new WeakSet();
4190
+ let entries = 0;
4191
+ const visit = (current, depth, sensitive = false) => {
4192
+ if (typeof current === "string") {
4193
+ if (sensitive && current.length > 0 && current !== REDACTED) return REDACTED;
4194
+ return redactText(current, options);
4195
+ }
4196
+ if (current === null || current === void 0 || typeof current === "number" || typeof current === "boolean" || typeof current === "bigint") {
4197
+ return current;
4198
+ }
4199
+ if (typeof current !== "object") return current;
4200
+ if (isBinary(current)) return current;
4201
+ if (current instanceof Date) return new Date(current.getTime());
4202
+ if (depth >= maxDepth) return TRUNCATED;
4203
+ if (ancestors.has(current)) return CIRCULAR;
4204
+ ancestors.add(current);
4205
+ try {
4206
+ return cloneObject(current, depth);
4207
+ } finally {
4208
+ ancestors.delete(current);
4209
+ }
4210
+ };
4211
+ const cloneObject = (current, depth) => {
4212
+ if (current instanceof Error) {
4213
+ const error = {
4214
+ name: current.name,
4215
+ message: visit(current.message, depth + 1),
4216
+ stack: visit(current.stack, depth + 1)
4217
+ };
4218
+ for (const [key, child] of Object.entries(current)) {
4219
+ error[key] = visit(child, depth + 1, SENSITIVE_NAME.test(key));
4220
+ }
4221
+ return error;
4222
+ }
4223
+ if (Array.isArray(current)) {
4224
+ const clone2 = [];
4225
+ for (const child of current) {
4226
+ if (++entries > maxEntries) {
4227
+ clone2.push(TRUNCATED);
4228
+ break;
4229
+ }
4230
+ clone2.push(visit(child, depth + 1));
4231
+ }
4232
+ return clone2;
4233
+ }
4234
+ const record = current;
4235
+ if (isMediaObject(record)) return { ...record };
4236
+ const clone = {};
4237
+ for (const [key, child] of Object.entries(record)) {
4238
+ if (++entries > maxEntries) {
4239
+ clone[TRUNCATED] = true;
4240
+ break;
4241
+ }
4242
+ clone[key] = visit(child, depth + 1, SENSITIVE_NAME.test(key));
4243
+ }
4244
+ return clone;
4245
+ };
4246
+ return visit(value, 0);
4247
+ }
4248
+
4249
+ // src/utils/json-post.ts
4250
+ var MAX_ERROR_MESSAGE_CHARS = 240;
4251
+ function abortError() {
4252
+ return new DOMException("Aborted", "AbortError");
4253
+ }
4254
+ function throwIfAborted(signal) {
4255
+ if (signal?.aborted) throw abortError();
4256
+ }
4257
+ async function postJson(request) {
4258
+ const { provider, signal } = request;
4259
+ const secrets = request.secrets?.filter((s) => typeof s === "string" && s !== "");
4260
+ const clean = (text2) => bound(redactText(text2, { secrets }));
4261
+ const fetchImpl = request.fetch ?? globalThis.fetch;
4262
+ throwIfAborted(signal);
4263
+ let response;
4264
+ try {
4265
+ response = await fetchImpl(request.url, {
4266
+ method: "POST",
4267
+ headers: request.headers,
4268
+ body: JSON.stringify(request.body),
4269
+ signal
4270
+ });
4271
+ } catch (err) {
4272
+ if (signal?.aborted || isAbortError2(err)) throw abortError();
4273
+ const detail = err instanceof Error ? err.message : String(err);
4274
+ throw new GGAIError(`Couldn't reach ${provider}: ${clean(detail)}`, {
4275
+ source: "network",
4276
+ cause: err
4277
+ });
4278
+ }
4279
+ const headerRequestId = readHeader(
4280
+ response.headers,
4281
+ "x-request-id",
4282
+ "request-id",
4283
+ "openai-request-id"
4284
+ );
4285
+ let text;
4286
+ try {
4287
+ text = await response.text();
4288
+ } catch (err) {
4289
+ if (signal?.aborted || isAbortError2(err)) throw abortError();
4290
+ throw new GGAIError(`Lost the connection to ${provider} while reading its response.`, {
4291
+ source: "network",
4292
+ cause: err
4293
+ });
4294
+ }
4295
+ if (!response.ok) {
4296
+ throw httpError(provider, response, text, headerRequestId, clean);
4297
+ }
4298
+ try {
4299
+ return {
4300
+ json: JSON.parse(text),
4301
+ ...headerRequestId ? { requestId: headerRequestId } : {}
4302
+ };
4303
+ } catch {
4304
+ throw new ProviderError(provider, `${provider} returned a response that isn't valid JSON.`, {
4305
+ statusCode: response.status,
4306
+ ...headerRequestId ? { requestId: headerRequestId } : {}
4307
+ });
4308
+ }
4309
+ }
4310
+ function httpError(provider, response, text, headerRequestId, clean) {
4311
+ const status = response.status;
4312
+ const parsed = parseErrorBody(text);
4313
+ const rawMessage = parsed.message ?? (parsed.isJson ? "" : text.trim());
4314
+ const message = !rawMessage ? emptyProviderErrorMessage(status) : isRawHtmlErrorEcho(rawMessage) ? providerHtmlErrorMessage(status) : clean(rawMessage);
4315
+ const requestId = headerRequestId ?? parsed.requestId;
4316
+ const base = {
4317
+ statusCode: status,
4318
+ ...requestId ? { requestId } : {}
4319
+ };
4320
+ if (status === 401 || status === 403) {
4321
+ return new ProviderError(provider, message, { ...base, source: "auth" });
4322
+ }
4323
+ const codeType = `${parsed.code ?? ""} ${parsed.type ?? ""}`.toLowerCase();
4324
+ if (status === 402 || codeType.includes("insufficient_quota") || isHardBillingMessage(message)) {
4325
+ const stamped = /usage limit reached/i.test(message) ? message : `usage limit reached: ${message}`;
4326
+ return new ProviderError(provider, stamped, base);
4327
+ }
4328
+ if (status === 429) {
4329
+ const retryAfter = Number(readHeader(response.headers, "retry-after"));
4330
+ const resetsAt = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.floor(Date.now() / 1e3) + Math.ceil(retryAfter) : void 0;
4331
+ return new ProviderError(provider, message, {
4332
+ ...base,
4333
+ ...resetsAt !== void 0 ? { resetsAt } : {}
4334
+ });
4335
+ }
4336
+ return new ProviderError(provider, message, base);
4337
+ }
4338
+ function parseErrorBody(text) {
4339
+ let parsed;
4340
+ try {
4341
+ parsed = JSON.parse(text);
4342
+ } catch {
4343
+ return { isJson: false };
4344
+ }
4345
+ if (!isRecord(parsed)) return { isJson: true };
4346
+ const error = parsed.error;
4347
+ const errorObj = isRecord(error) ? error : void 0;
4348
+ const message = stringField(errorObj, "message") ?? (typeof error === "string" && error.trim() ? error.trim() : void 0) ?? stringField(parsed, "message") ?? stringField(parsed, "detail");
4349
+ const requestId = stringField(parsed, "request_id") ?? stringField(errorObj, "request_id");
4350
+ const code = stringField(errorObj, "code") ?? stringField(errorObj, "status");
4351
+ const type = stringField(errorObj, "type");
4352
+ return {
4353
+ isJson: true,
4354
+ ...message ? { message } : {},
4355
+ ...requestId ? { requestId } : {},
4356
+ ...code ? { code } : {},
4357
+ ...type ? { type } : {}
4358
+ };
4359
+ }
4360
+ function stringField(record, key) {
4361
+ const value = record?.[key];
4362
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
4363
+ }
4364
+ function isRecord(value) {
4365
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4366
+ }
4367
+ function isAbortError2(err) {
4368
+ return (err instanceof Error || err instanceof DOMException) && err.name === "AbortError";
4369
+ }
4370
+ function bound(text) {
4371
+ const trimmed = text.trim();
4372
+ return trimmed.length > MAX_ERROR_MESSAGE_CHARS ? `${trimmed.slice(0, MAX_ERROR_MESSAGE_CHARS)}\u2026` : trimmed;
4373
+ }
4374
+ function joinUrl(baseUrl, path) {
4375
+ return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
4376
+ }
4377
+
4378
+ // src/providers/gemini-embeddings.ts
4379
+ var GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
4380
+ var GEMINI_EMBED_BATCH_SIZE = 100;
4381
+ var TASK_TYPES = {
4382
+ query: "RETRIEVAL_QUERY",
4383
+ document: "RETRIEVAL_DOCUMENT"
4384
+ };
4385
+ function supportsTaskType(model) {
4386
+ return !/^gemini-embedding-2(?:$|[-.@])/.test(model);
4387
+ }
4388
+ async function embedGemini(request) {
4389
+ if (!request.apiKey) {
4390
+ throw new GGAIError("Gemini embeddings need a Gemini API key (apiKey).", {
4391
+ source: "auth",
4392
+ hint: "Create a key in Google AI Studio. Gemini Code Assist sign-in can't be used for embeddings."
4393
+ });
4394
+ }
4395
+ const model = request.model.replace(/^models\//, "");
4396
+ const resource = `models/${model}`;
4397
+ const taskType = request.inputType && supportsTaskType(model) ? TASK_TYPES[request.inputType] : void 0;
4398
+ const embedContentConfig = {
4399
+ ...taskType ? { taskType } : {},
4400
+ ...request.dimensions !== void 0 ? { outputDimensionality: request.dimensions } : {}
4401
+ };
4402
+ const body = mergeProviderOptions(
4403
+ {
4404
+ requests: request.input.map((text) => ({
4405
+ model: resource,
4406
+ content: { parts: [{ text }] },
4407
+ ...Object.keys(embedContentConfig).length > 0 ? { embedContentConfig } : {}
4408
+ }))
4409
+ },
4410
+ request.providerOptions
4411
+ );
4412
+ const { json, requestId } = await postJson({
4413
+ provider: "gemini",
4414
+ url: joinUrl(request.baseUrl ?? GEMINI_API_BASE_URL, `${resource}:batchEmbedContents`),
4415
+ headers: { "Content-Type": "application/json", "x-goog-api-key": request.apiKey },
4416
+ body,
4417
+ fetch: request.fetch,
4418
+ signal: request.signal,
4419
+ secrets: [request.apiKey]
4420
+ });
4421
+ const invalid = (detail) => new ProviderError("gemini", `Invalid embeddings response: ${detail}.`, {
4422
+ ...requestId ? { requestId } : {}
4423
+ });
4424
+ if (!isRecord(json) || !Array.isArray(json.embeddings)) {
4425
+ throw invalid("missing `embeddings` array");
4426
+ }
4427
+ const embeddings = json.embeddings.map((entry) => {
4428
+ if (!isRecord(entry) || !Array.isArray(entry.values)) {
4429
+ throw invalid("an entry has no `values` array");
4430
+ }
4431
+ return entry.values;
4432
+ });
4433
+ const usage = json.usageMetadata;
4434
+ const promptTokens = isRecord(usage) ? usage.promptTokenCount : void 0;
4435
+ return {
4436
+ embeddings,
4437
+ model,
4438
+ ...typeof promptTokens === "number" && Number.isFinite(promptTokens) ? { usage: { inputTokens: promptTokens } } : {}
4439
+ };
4440
+ }
4441
+
4442
+ // src/providers/openai-compat-retrieval.ts
4443
+ function jsonHeaders(apiKey) {
4444
+ return {
4445
+ "Content-Type": "application/json",
4446
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
4447
+ };
4448
+ }
4449
+ async function embedOpenAICompatible(request, config) {
4450
+ const body = mergeProviderOptions(
4451
+ {
4452
+ model: config.wireModel ?? request.model,
4453
+ input: request.input,
4454
+ // gg-ai parses float arrays only; never let a server default to base64.
4455
+ encoding_format: "float",
4456
+ ...request.dimensions !== void 0 ? { dimensions: request.dimensions } : {},
4457
+ ...request.inputType && config.inputTypeFields ? config.inputTypeFields(request.inputType) : {}
4458
+ },
4459
+ request.providerOptions
4460
+ );
4461
+ const { json, requestId } = await postJson({
4462
+ provider: config.provider,
4463
+ url: joinUrl(config.baseUrl, "embeddings"),
4464
+ headers: jsonHeaders(request.apiKey),
4465
+ body,
4466
+ fetch: request.fetch,
4467
+ signal: request.signal,
4468
+ secrets: [request.apiKey]
4469
+ });
4470
+ const invalid = (detail) => new ProviderError(config.provider, `Invalid embeddings response: ${detail}.`, {
4471
+ ...requestId ? { requestId } : {}
4472
+ });
4473
+ if (!isRecord(json) || !Array.isArray(json.data)) throw invalid("missing `data` array");
4474
+ const items = json.data.map((item) => {
4475
+ if (!isRecord(item)) throw invalid("an entry in `data` is not an object");
4476
+ const index = item.index;
4477
+ if (index !== void 0 && !(typeof index === "number" && Number.isInteger(index))) {
4478
+ throw invalid("an entry has a non-integer `index`");
4479
+ }
4480
+ if (!Array.isArray(item.embedding)) {
4481
+ throw invalid("an entry's `embedding` is not an array of numbers");
4482
+ }
4483
+ return { index, embedding: item.embedding };
4484
+ });
4485
+ const withIndex = items.filter((item) => item.index !== void 0).length;
4486
+ let embeddings;
4487
+ if (withIndex === 0) {
4488
+ embeddings = items.map((item) => item.embedding);
4489
+ } else if (withIndex !== items.length) {
4490
+ throw invalid("some entries have an `index` and some don't");
4491
+ } else {
4492
+ const slots = new Array(items.length);
4493
+ for (const item of items) {
4494
+ const index = item.index;
4495
+ if (index < 0 || index >= items.length || slots[index] !== void 0) {
4496
+ throw invalid(`unexpected or duplicate index ${index}`);
4497
+ }
4498
+ slots[index] = item.embedding;
4499
+ }
4500
+ embeddings = slots;
4501
+ }
4502
+ const usage = json.usage;
4503
+ const promptTokens = isRecord(usage) ? usage.prompt_tokens : void 0;
4504
+ return {
4505
+ embeddings,
4506
+ ...typeof json.model === "string" && json.model ? { model: json.model } : {},
4507
+ ...typeof promptTokens === "number" && Number.isFinite(promptTokens) ? { usage: { inputTokens: promptTokens } } : {}
4508
+ };
4509
+ }
4510
+ async function rerankCohereCompatible(request, config) {
4511
+ const body = mergeProviderOptions(
4512
+ {
4513
+ model: config.wireModel ?? request.model,
4514
+ query: request.query,
4515
+ documents: request.documents,
4516
+ ...request.topN !== void 0 ? { top_n: request.topN } : {}
4517
+ },
4518
+ request.providerOptions
4519
+ );
4520
+ const { json, requestId } = await postJson({
4521
+ provider: config.provider,
4522
+ url: joinUrl(config.baseUrl, "rerank"),
4523
+ headers: jsonHeaders(request.apiKey),
4524
+ body,
4525
+ fetch: request.fetch,
4526
+ signal: request.signal,
4527
+ secrets: [request.apiKey]
4528
+ });
4529
+ const invalid = (detail) => new ProviderError(config.provider, `Invalid rerank response: ${detail}.`, {
4530
+ ...requestId ? { requestId } : {}
4531
+ });
4532
+ if (!isRecord(json) || !Array.isArray(json.results)) throw invalid("missing `results` array");
4533
+ const results = json.results.map((item) => {
4534
+ if (!isRecord(item)) throw invalid("an entry in `results` is not an object");
4535
+ const { index, relevance_score: score } = item;
4536
+ if (typeof index !== "number" || typeof score !== "number") {
4537
+ throw invalid("an entry is missing a numeric `index` or `relevance_score`");
4538
+ }
4539
+ return { index, score };
4540
+ });
4541
+ const usage = json.usage;
4542
+ const tokens = isRecord(usage) ? usage.prompt_tokens ?? usage.total_tokens : void 0;
4543
+ const searchUnits = isRecord(usage) ? usage.search_units : void 0;
4544
+ const rerankUsage = {
4545
+ ...typeof tokens === "number" && Number.isFinite(tokens) ? { inputTokens: tokens } : {},
4546
+ ...typeof searchUnits === "number" && Number.isFinite(searchUnits) ? { searchUnits } : {}
4547
+ };
4548
+ return {
4549
+ results,
4550
+ ...typeof json.model === "string" && json.model ? { model: json.model } : {},
4551
+ ...Object.keys(rerankUsage).length > 0 ? { usage: rerankUsage } : {}
4552
+ };
4553
+ }
4554
+
4064
4555
  // src/provider-registry.ts
4065
4556
  var ProviderRegistryImpl = class {
4066
4557
  providers = /* @__PURE__ */ new Map();
@@ -4090,6 +4581,11 @@ var ProviderRegistryImpl = class {
4090
4581
  has(name) {
4091
4582
  return this.providers.has(name);
4092
4583
  }
4584
+ /** Whether a registered provider offers a capability (`false` for unknown providers). */
4585
+ supports(name, capability) {
4586
+ const entry = this.providers.get(name);
4587
+ return typeof entry?.[capability] === "function";
4588
+ }
4093
4589
  /** List all registered provider names. */
4094
4590
  list() {
4095
4591
  return [...this.providers.keys()];
@@ -4267,6 +4763,10 @@ function observePreparedContext(before, after, tools) {
4267
4763
  }
4268
4764
 
4269
4765
  // src/stream.ts
4766
+ var OPENAI_EMBED_BATCH_SIZE = 2048;
4767
+ var OPENROUTER_EMBED_BATCH_SIZE = 64;
4768
+ var LOCAL_EMBED_BATCH_SIZE = 64;
4769
+ var OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
4270
4770
  var GLM_CODING_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
4271
4771
  var KIMI_CODE_USER_AGENT = `kimi-code-cli/${process.env.KIMI_CODE_VERSION ?? "1.0.11"}`;
4272
4772
  var GROK_CLI_PROXY_HOST = "cli-chat-proxy.grok.com";
@@ -4287,10 +4787,20 @@ providerRegistry.register("openai", {
4287
4787
  return streamOpenAICodex(options);
4288
4788
  }
4289
4789
  return streamOpenAI(options);
4290
- }
4790
+ },
4791
+ // OpenAI API-key auth only; ChatGPT-subscription (Codex) tokens have no embeddings API.
4792
+ // OpenAI has no task/input-type parameter, so `inputType` is ignored.
4793
+ embed: (request) => embedOpenAICompatible(request, {
4794
+ provider: "openai",
4795
+ baseUrl: request.baseUrl ?? "https://api.openai.com/v1"
4796
+ }),
4797
+ embedBatchSize: OPENAI_EMBED_BATCH_SIZE
4291
4798
  });
4292
4799
  providerRegistry.register("gemini", {
4293
- stream: (options) => streamGemini(options)
4800
+ stream: (options) => streamGemini(options),
4801
+ // Native Gemini API with an AI Studio key — not the Code Assist OAuth endpoint above.
4802
+ embed: (request) => embedGemini(request),
4803
+ embedBatchSize: GEMINI_EMBED_BATCH_SIZE
4294
4804
  });
4295
4805
  providerRegistry.register("glm", {
4296
4806
  stream: (options) => streamOpenAI({
@@ -4314,7 +4824,19 @@ providerRegistry.register("deepseek", {
4314
4824
  providerRegistry.register("openrouter", {
4315
4825
  stream: (options) => streamOpenAI({
4316
4826
  ...options,
4317
- baseUrl: options.baseUrl ?? "https://openrouter.ai/api/v1"
4827
+ baseUrl: options.baseUrl ?? OPENROUTER_BASE_URL
4828
+ }),
4829
+ embed: (request) => embedOpenAICompatible(request, {
4830
+ provider: "openrouter",
4831
+ baseUrl: request.baseUrl ?? OPENROUTER_BASE_URL,
4832
+ inputTypeFields: (inputType) => ({
4833
+ input_type: inputType === "query" ? "search_query" : "search_document"
4834
+ })
4835
+ }),
4836
+ embedBatchSize: OPENROUTER_EMBED_BATCH_SIZE,
4837
+ rerank: (request) => rerankCohereCompatible(request, {
4838
+ provider: "openrouter",
4839
+ baseUrl: request.baseUrl ?? OPENROUTER_BASE_URL
4318
4840
  })
4319
4841
  });
4320
4842
  providerRegistry.register("huggingface", {
@@ -4384,19 +4906,36 @@ providerRegistry.register("local", {
4384
4906
  // vLLM). There is no default endpoint: the baseUrl comes from the endpoint
4385
4907
  // credential the discovery layer wrote, so a missing one is a wiring bug, not
4386
4908
  // something to paper over with a guess at someone else's port.
4387
- stream: (options) => {
4388
- if (!options.baseUrl) {
4389
- throw new GGAIError(
4390
- "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."
4391
- );
4392
- }
4393
- return streamOpenAI({
4394
- ...options,
4395
- model: localWireModelId(options.model),
4396
- webSearch: false
4397
- });
4398
- }
4909
+ stream: (options) => streamOpenAI({
4910
+ ...options,
4911
+ baseUrl: requireLocalBaseUrl(options.baseUrl),
4912
+ model: localWireModelId(options.model),
4913
+ webSearch: false
4914
+ }),
4915
+ // Every supported local server exposes OpenAI-compatible `/v1/embeddings`
4916
+ // (the model must be an embedding model). None has an input-type parameter.
4917
+ embed: (request) => embedOpenAICompatible(request, {
4918
+ provider: "local",
4919
+ baseUrl: requireLocalBaseUrl(request.baseUrl),
4920
+ wireModel: localWireModelId(request.model)
4921
+ }),
4922
+ embedBatchSize: LOCAL_EMBED_BATCH_SIZE,
4923
+ // llama.cpp (`llama-server --rerank`) and vLLM serve `/v1/rerank`; Ollama and
4924
+ // LM Studio don't, and answer with a 404 ProviderError.
4925
+ rerank: (request) => rerankCohereCompatible(request, {
4926
+ provider: "local",
4927
+ baseUrl: requireLocalBaseUrl(request.baseUrl),
4928
+ wireModel: localWireModelId(request.model)
4929
+ })
4399
4930
  });
4931
+ function requireLocalBaseUrl(baseUrl) {
4932
+ if (!baseUrl) {
4933
+ throw new GGAIError(
4934
+ "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."
4935
+ );
4936
+ }
4937
+ return baseUrl;
4938
+ }
4400
4939
  function stream(options) {
4401
4940
  const entry = providerRegistry.get(options.provider);
4402
4941
  if (!entry) {
@@ -4455,6 +4994,121 @@ function messagesContainVideo(messages) {
4455
4994
  return false;
4456
4995
  }
4457
4996
 
4997
+ // src/embed.ts
4998
+ var CAPABILITY_LABEL = {
4999
+ embed: "embeddings",
5000
+ rerank: "reranking"
5001
+ };
5002
+ function resolveCapability(provider, capability) {
5003
+ const entry = providerRegistry.get(provider);
5004
+ if (!entry) {
5005
+ throw new GGAIError(
5006
+ `Unknown provider: "${provider}". Registered: ${providerRegistry.list().join(", ")}`
5007
+ );
5008
+ }
5009
+ const fn = entry[capability];
5010
+ if (typeof fn !== "function") {
5011
+ const label = CAPABILITY_LABEL[capability];
5012
+ const offering = providerRegistry.list().filter((name) => providerRegistry.supports(name, capability)).sort();
5013
+ throw new GGAIError(`Provider "${provider}" doesn't support ${label}.`, {
5014
+ source: "capability",
5015
+ hint: offering.length > 0 ? `Use a provider that offers ${label}: ${offering.join(", ")}.` : `No registered provider offers ${label}.`
5016
+ });
5017
+ }
5018
+ return fn;
5019
+ }
5020
+ function assertPositiveInteger(name, value) {
5021
+ if (value !== void 0 && !(Number.isInteger(value) && value > 0)) {
5022
+ throw new GGAIError(`${name} must be a positive integer (got ${String(value)}).`);
5023
+ }
5024
+ }
5025
+ async function embed(options) {
5026
+ const embedFn = resolveCapability(options.provider, "embed");
5027
+ if (!Array.isArray(options.input)) {
5028
+ throw new GGAIError("embed() input must be an array of strings.");
5029
+ }
5030
+ assertPositiveInteger("dimensions", options.dimensions);
5031
+ assertPositiveInteger("batchSize", options.batchSize);
5032
+ if (options.input.length === 0) return { embeddings: [], model: options.model };
5033
+ const entry = providerRegistry.get(options.provider);
5034
+ const batchSize = options.batchSize ?? entry?.embedBatchSize ?? options.input.length;
5035
+ const { normalize, batchSize: _batchSize, ...request } = options;
5036
+ const embeddings = [];
5037
+ let model = options.model;
5038
+ let inputTokens;
5039
+ let expectedLength = options.dimensions;
5040
+ for (let start = 0; start < options.input.length; start += batchSize) {
5041
+ throwIfAborted(options.signal);
5042
+ const batch = options.input.slice(start, start + batchSize);
5043
+ const response = await embedFn({ ...request, input: batch });
5044
+ const invalid = (detail) => new ProviderError(options.provider, `Invalid embeddings response: ${detail}.`);
5045
+ if (!Array.isArray(response.embeddings) || response.embeddings.length !== batch.length) {
5046
+ const count = Array.isArray(response.embeddings) ? response.embeddings.length : 0;
5047
+ throw invalid(`expected ${batch.length} embeddings, got ${count}`);
5048
+ }
5049
+ for (const vector of response.embeddings) {
5050
+ if (!Array.isArray(vector) || vector.length === 0) throw invalid("received an empty vector");
5051
+ if (!vector.every((value) => typeof value === "number" && Number.isFinite(value))) {
5052
+ throw invalid("received a vector with non-numeric values");
5053
+ }
5054
+ if (expectedLength === void 0) {
5055
+ expectedLength = vector.length;
5056
+ } else if (vector.length !== expectedLength) {
5057
+ throw invalid(
5058
+ options.dimensions !== void 0 ? `requested ${options.dimensions} dimensions, got a vector with ${vector.length}` : `vectors have different lengths (${expectedLength} and ${vector.length})`
5059
+ );
5060
+ }
5061
+ embeddings.push(normalize ? l2Normalize(vector) : vector);
5062
+ }
5063
+ if (response.model) model = response.model;
5064
+ if (response.usage) inputTokens = (inputTokens ?? 0) + response.usage.inputTokens;
5065
+ }
5066
+ return {
5067
+ embeddings,
5068
+ model,
5069
+ ...inputTokens !== void 0 ? { usage: { inputTokens } } : {}
5070
+ };
5071
+ }
5072
+ async function rerank(options) {
5073
+ const rerankFn = resolveCapability(options.provider, "rerank");
5074
+ if (!Array.isArray(options.documents)) {
5075
+ throw new GGAIError("rerank() documents must be an array of strings.");
5076
+ }
5077
+ assertPositiveInteger("topN", options.topN);
5078
+ if (options.documents.length === 0) return { results: [], model: options.model };
5079
+ throwIfAborted(options.signal);
5080
+ const response = await rerankFn(options);
5081
+ const invalid = (detail) => new ProviderError(options.provider, `Invalid rerank response: ${detail}.`);
5082
+ if (!Array.isArray(response.results)) throw invalid("missing results");
5083
+ const seen = /* @__PURE__ */ new Set();
5084
+ const results = response.results.map(({ index, score }) => {
5085
+ if (!Number.isInteger(index) || index < 0 || index >= options.documents.length) {
5086
+ throw invalid(
5087
+ `index ${String(index)} is out of range for ${options.documents.length} documents`
5088
+ );
5089
+ }
5090
+ if (seen.has(index)) throw invalid(`index ${index} appears more than once`);
5091
+ if (typeof score !== "number" || !Number.isFinite(score)) {
5092
+ throw invalid(`document ${index} has a non-numeric score`);
5093
+ }
5094
+ seen.add(index);
5095
+ return { index, score };
5096
+ });
5097
+ results.sort((a, b) => b.score - a.score || a.index - b.index);
5098
+ return {
5099
+ results: options.topN !== void 0 ? results.slice(0, options.topN) : results,
5100
+ model: response.model ?? options.model,
5101
+ ...response.usage ? { usage: response.usage } : {}
5102
+ };
5103
+ }
5104
+ function l2Normalize(vector) {
5105
+ let sumOfSquares = 0;
5106
+ for (const value of vector) sumOfSquares += value * value;
5107
+ if (sumOfSquares === 0) return [...vector];
5108
+ const norm = Math.sqrt(sumOfSquares);
5109
+ return vector.map((value) => value / norm);
5110
+ }
5111
+
4458
5112
  // src/chat-error.ts
4459
5113
  function causeCode(error) {
4460
5114
  if (!(error instanceof Error) || !error.cause || typeof error.cause !== "object") return "";
@@ -4696,143 +5350,6 @@ Original: ${message}`;
4696
5350
  return message;
4697
5351
  }
4698
5352
 
4699
- // src/redaction.ts
4700
- var REDACTED = "[REDACTED]";
4701
- var TRUNCATED = "[TRUNCATED]";
4702
- var CIRCULAR = "[CIRCULAR]";
4703
- var SENSITIVE_NAME = /(?:^|[_-])(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|key|auth(?:orization)?|bearer|cookie|credential|private[_-]?key|password|passwd|secret)(?:$|[_-])/i;
4704
- 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;
4705
- 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;
4706
- var URL_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#@:"'`<>]*:[^\s/?#"'`<>]+@/gi;
4707
- var URL_USERINFO_TO_FIRST_AT = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]*:[^\s/@]+@/gi;
4708
- function escaped(value) {
4709
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4710
- }
4711
- function normalizedSecrets(secrets) {
4712
- if (!secrets) return [];
4713
- return [...new Set([...secrets].filter((value) => value.length >= 8 && value !== REDACTED))].sort(
4714
- (a, b) => b.length - a.length
4715
- );
4716
- }
4717
- function environmentSecrets(env) {
4718
- const values = /* @__PURE__ */ new Set();
4719
- for (const [name, value] of Object.entries(env)) {
4720
- if (!value || value.length < 8 || value === REDACTED || !SENSITIVE_NAME.test(name)) continue;
4721
- values.add(value);
4722
- }
4723
- return [...values].sort((a, b) => b.length - a.length);
4724
- }
4725
- function redactText(text, options = {}) {
4726
- let result = text;
4727
- result = result.replace(
4728
- /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g,
4729
- REDACTED
4730
- );
4731
- result = result.replace(URL_USERINFO, `$1${REDACTED}@`);
4732
- result = result.replace(URL_USERINFO_TO_FIRST_AT, `$1${REDACTED}@`);
4733
- result = result.replace(
4734
- /\b(authorization\s*[:=]\s*)(?:bearer|basic)\s+[^\s,;]+/gi,
4735
- `$1${REDACTED}`
4736
- );
4737
- result = result.replace(
4738
- /\b(bearer|basic)\s+(?![A-Za-z][a-z]{0,14}\b)[A-Za-z0-9+/_.=-]{8,}/gi,
4739
- `$1 ${REDACTED}`
4740
- );
4741
- result = result.replace(/\b(cookie|set-cookie)(\s*:\s*)[^\s=;:]+=[^\r\n]*/gi, `$1$2${REDACTED}`);
4742
- result = result.replace(
4743
- /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g,
4744
- REDACTED
4745
- );
4746
- result = result.replace(
4747
- /\b(?:sk-(?:ant-|proj-)?|xox[baprs]-|gh[pousr]_|github_pat_|AIza)[A-Za-z0-9_-]{12,}\b/g,
4748
- REDACTED
4749
- );
4750
- result = result.replace(
4751
- ENV_SECRET_ASSIGNMENT,
4752
- (_match, name, separator, quote) => `${name}${separator}${quote}${REDACTED}${quote}`
4753
- );
4754
- result = result.replace(
4755
- COMPACT_SECRET_ASSIGNMENT,
4756
- (_match, name, quote) => `${name}=${quote}${REDACTED}${quote}`
4757
- );
4758
- for (const secret of normalizedSecrets(options.secrets)) {
4759
- result = result.replace(new RegExp(escaped(secret), "g"), REDACTED);
4760
- }
4761
- const maxStringLength = options.maxStringLength ?? 1e6;
4762
- if (result.length > maxStringLength) {
4763
- result = `${result.slice(0, maxStringLength)}${TRUNCATED}`;
4764
- }
4765
- return result;
4766
- }
4767
- function isBinary(value) {
4768
- return value instanceof ArrayBuffer || ArrayBuffer.isView(value) || typeof Blob !== "undefined" && value instanceof Blob;
4769
- }
4770
- function isMediaObject(value) {
4771
- return (value.type === "image" || value.type === "video") && (typeof value.data === "string" || typeof value.url === "string");
4772
- }
4773
- function redactValue(value, options = {}) {
4774
- const maxDepth = options.maxDepth ?? 20;
4775
- const maxEntries = options.maxEntries ?? 1e4;
4776
- const ancestors = /* @__PURE__ */ new WeakSet();
4777
- let entries = 0;
4778
- const visit = (current, depth, sensitive = false) => {
4779
- if (typeof current === "string") {
4780
- if (sensitive && current.length > 0 && current !== REDACTED) return REDACTED;
4781
- return redactText(current, options);
4782
- }
4783
- if (current === null || current === void 0 || typeof current === "number" || typeof current === "boolean" || typeof current === "bigint") {
4784
- return current;
4785
- }
4786
- if (typeof current !== "object") return current;
4787
- if (isBinary(current)) return current;
4788
- if (current instanceof Date) return new Date(current.getTime());
4789
- if (depth >= maxDepth) return TRUNCATED;
4790
- if (ancestors.has(current)) return CIRCULAR;
4791
- ancestors.add(current);
4792
- try {
4793
- return cloneObject(current, depth);
4794
- } finally {
4795
- ancestors.delete(current);
4796
- }
4797
- };
4798
- const cloneObject = (current, depth) => {
4799
- if (current instanceof Error) {
4800
- const error = {
4801
- name: current.name,
4802
- message: visit(current.message, depth + 1),
4803
- stack: visit(current.stack, depth + 1)
4804
- };
4805
- for (const [key, child] of Object.entries(current)) {
4806
- error[key] = visit(child, depth + 1, SENSITIVE_NAME.test(key));
4807
- }
4808
- return error;
4809
- }
4810
- if (Array.isArray(current)) {
4811
- const clone2 = [];
4812
- for (const child of current) {
4813
- if (++entries > maxEntries) {
4814
- clone2.push(TRUNCATED);
4815
- break;
4816
- }
4817
- clone2.push(visit(child, depth + 1));
4818
- }
4819
- return clone2;
4820
- }
4821
- const record = current;
4822
- if (isMediaObject(record)) return { ...record };
4823
- const clone = {};
4824
- for (const [key, child] of Object.entries(record)) {
4825
- if (++entries > maxEntries) {
4826
- clone[TRUNCATED] = true;
4827
- break;
4828
- }
4829
- clone[key] = visit(child, depth + 1, SENSITIVE_NAME.test(key));
4830
- }
4831
- return clone;
4832
- };
4833
- return visit(value, 0);
4834
- }
4835
-
4836
5353
  // src/providers/palsu.ts
4837
5354
  function palsuText(text) {
4838
5355
  return { role: "assistant", content: text ? [{ type: "text", text }] : [] };
@@ -4913,7 +5430,8 @@ function computeCacheUsage(current, previous) {
4913
5430
  function registerPalsuProvider(config) {
4914
5431
  const name = config?.name ?? "palsu";
4915
5432
  const responses = [];
4916
- const state = { callCount: 0 };
5433
+ const state = { callCount: 0, embedCallCount: 0, rerankCallCount: 0 };
5434
+ const embedDimensions = config?.embedDimensions ?? PALSU_DEFAULT_EMBED_DIMENSIONS;
4917
5435
  const defaultResponse = config?.defaultResponse ?? palsuText("");
4918
5436
  const enableCache = config?.promptCache ?? false;
4919
5437
  let lastMessagesSerialized = null;
@@ -4939,10 +5457,11 @@ function registerPalsuProvider(config) {
4939
5457
  },
4940
5458
  state,
4941
5459
  getModel(modelName) {
4942
- if (!modelStates.has(modelName)) {
4943
- modelStates.set(modelName, { responses: [] });
5460
+ let ms = modelStates.get(modelName);
5461
+ if (!ms) {
5462
+ ms = { responses: [] };
5463
+ modelStates.set(modelName, ms);
4944
5464
  }
4945
- const ms = modelStates.get(modelName);
4946
5465
  return {
4947
5466
  setResponses(r) {
4948
5467
  ms.responses.length = 0;
@@ -4980,10 +5499,72 @@ function registerPalsuProvider(config) {
4980
5499
  return yield* simulateStream(message, stopReason, options.signal, cacheUsage);
4981
5500
  })();
4982
5501
  return new StreamResult(gen, options.signal);
5502
+ },
5503
+ async embed(request) {
5504
+ throwIfAborted(request.signal);
5505
+ state.embedCallCount++;
5506
+ const dimensions = request.dimensions ?? embedDimensions;
5507
+ return {
5508
+ embeddings: request.input.map((text) => palsuEmbedding(text, dimensions)),
5509
+ model: request.model,
5510
+ usage: { inputTokens: request.input.reduce((n, text) => n + tokenize(text).length, 0) }
5511
+ };
5512
+ },
5513
+ ...config?.embedBatchSize !== void 0 ? { embedBatchSize: config.embedBatchSize } : {},
5514
+ async rerank(request) {
5515
+ throwIfAborted(request.signal);
5516
+ state.rerankCallCount++;
5517
+ return {
5518
+ results: request.documents.map((document, index) => ({
5519
+ index,
5520
+ score: palsuRerankScore(request.query, document)
5521
+ })),
5522
+ model: request.model
5523
+ };
4983
5524
  }
4984
5525
  });
4985
5526
  return handle;
4986
5527
  }
5528
+ var PALSU_DEFAULT_EMBED_DIMENSIONS = 64;
5529
+ function tokenize(text) {
5530
+ return text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
5531
+ }
5532
+ function fnv1a(text) {
5533
+ let hash = 2166136261;
5534
+ for (let i = 0; i < text.length; i++) {
5535
+ hash ^= text.charCodeAt(i);
5536
+ hash = Math.imul(hash, 16777619);
5537
+ }
5538
+ return hash >>> 0;
5539
+ }
5540
+ function palsuEmbedding(text, dimensions = PALSU_DEFAULT_EMBED_DIMENSIONS) {
5541
+ const vector = new Array(dimensions).fill(0);
5542
+ const tokens = tokenize(text);
5543
+ if (tokens.length === 0) {
5544
+ vector[0] = 1;
5545
+ return vector;
5546
+ }
5547
+ for (const token of tokens) {
5548
+ const hash = fnv1a(token);
5549
+ const slot = hash % dimensions;
5550
+ vector[slot] = (vector[slot] ?? 0) + (hash >>> 31 === 0 ? 1 : -1);
5551
+ }
5552
+ const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
5553
+ if (norm === 0) {
5554
+ vector.fill(0);
5555
+ vector[0] = 1;
5556
+ return vector;
5557
+ }
5558
+ return vector.map((value) => value / norm);
5559
+ }
5560
+ function palsuRerankScore(query, document) {
5561
+ const queryTokens = new Set(tokenize(query));
5562
+ if (queryTokens.size === 0) return 0;
5563
+ const documentTokens = new Set(tokenize(document));
5564
+ let hits = 0;
5565
+ for (const token of queryTokens) if (documentTokens.has(token)) hits++;
5566
+ return hits / queryTokens.size;
5567
+ }
4987
5568
  // Annotate the CommonJS export names for ESM import in node:
4988
5569
  0 && (module.exports = {
4989
5570
  EventStream,
@@ -4995,6 +5576,7 @@ function registerPalsuProvider(config) {
4995
5576
  clampProviderContextImages,
4996
5577
  classifyProviderError,
4997
5578
  dropInvalidToolCalls,
5579
+ embed,
4998
5580
  environmentSecrets,
4999
5581
  formatChatError,
5000
5582
  formatError,
@@ -5005,6 +5587,8 @@ function registerPalsuProvider(config) {
5005
5587
  isValidToolCallName,
5006
5588
  localWireModelId,
5007
5589
  palsuAssistantMessage,
5590
+ palsuEmbedding,
5591
+ palsuRerankScore,
5008
5592
  palsuText,
5009
5593
  palsuThinking,
5010
5594
  palsuToolCall,
@@ -5013,6 +5597,7 @@ function registerPalsuProvider(config) {
5013
5597
  redactText,
5014
5598
  redactValue,
5015
5599
  registerPalsuProvider,
5600
+ rerank,
5016
5601
  resolveToolSchema,
5017
5602
  sanitizeMessagesForWire,
5018
5603
  setProviderDiagnostic,