@kenkaiiii/gg-ai 5.76.0 → 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.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();
@@ -1677,7 +1717,7 @@ async function* runStream(options) {
1677
1717
  }
1678
1718
  maxTokens = 1;
1679
1719
  }
1680
- const params = {
1720
+ const baseParams = {
1681
1721
  model: options.model,
1682
1722
  max_tokens: maxTokens,
1683
1723
  messages,
@@ -1720,6 +1760,7 @@ async function* runStream(options) {
1720
1760
  ...useServerFallback ? { fallbacks: "default" } : {},
1721
1761
  stream: useStreaming
1722
1762
  };
1763
+ const params = mergeProviderOptions(baseParams, options.providerOptions);
1723
1764
  const hasAdaptiveThinking = isAdaptiveThinkingModel(options.model);
1724
1765
  const betaHeaders = [
1725
1766
  ...isOAuth ? ["claude-code-20250219", "oauth-2025-04-20"] : [],
@@ -2425,20 +2466,21 @@ async function* runStream2(options) {
2425
2466
  params.thinking = { type: "disabled" };
2426
2467
  }
2427
2468
  }
2469
+ const requestParams = mergeProviderOptions(params, options.providerOptions);
2428
2470
  if (getEnvironment()?.GGAI_DUMP_REQUEST) {
2429
2471
  const fs = await import("fs");
2430
2472
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2431
2473
  const dumpPath = `/tmp/ggai-request-${ts}.json`;
2432
- fs.writeFileSync(dumpPath, JSON.stringify(params, null, 2));
2474
+ fs.writeFileSync(dumpPath, JSON.stringify(requestParams, null, 2));
2433
2475
  fs.appendFileSync(
2434
2476
  "/tmp/ggai-requests.log",
2435
- `[${ts}] ${dumpPath} messages=${params.messages.length}
2477
+ `[${ts}] ${dumpPath} messages=${requestParams.messages.length}
2436
2478
  `
2437
2479
  );
2438
2480
  }
2439
2481
  if (!useStreaming) {
2440
2482
  try {
2441
- const completion = await client.chat.completions.create(params, {
2483
+ const completion = await client.chat.completions.create(requestParams, {
2442
2484
  signal: options.signal ?? void 0
2443
2485
  });
2444
2486
  yield* synthesizeEventsFromCompletion(completion, !!options.thinking, endpointKey);
@@ -2449,7 +2491,7 @@ async function* runStream2(options) {
2449
2491
  }
2450
2492
  let stream2;
2451
2493
  try {
2452
- stream2 = await client.chat.completions.create(params, {
2494
+ stream2 = await client.chat.completions.create(requestParams, {
2453
2495
  signal: options.signal ?? void 0
2454
2496
  });
2455
2497
  } catch (err) {
@@ -2937,7 +2979,9 @@ async function* runStream3(options, retriedWithoutReasoning = false) {
2937
2979
  headers["session_id"] = transportSessionId;
2938
2980
  headers["x-client-request-id"] = transportSessionId;
2939
2981
  }
2940
- const encodedRequest = await encodeCodexRequest(body);
2982
+ const encodedRequest = await encodeCodexRequest(
2983
+ mergeProviderOptions(body, options.providerOptions)
2984
+ );
2941
2985
  if (encodedRequest.compressed) headers["Content-Encoding"] = "zstd";
2942
2986
  providerDiag("codex_request_body", {
2943
2987
  rawBytes: encodedRequest.rawBytes,
@@ -3652,11 +3696,11 @@ function stripUnsupportedSchemaFields(value) {
3652
3696
  ["exclusiveMinimum", "minimum", 1],
3653
3697
  ["exclusiveMaximum", "maximum", -1]
3654
3698
  ]) {
3655
- const bound = value[key];
3656
- if (bound === void 0) continue;
3699
+ const bound2 = value[key];
3700
+ if (bound2 === void 0) continue;
3657
3701
  delete value[key];
3658
- if (typeof bound === "number" && value[target] === void 0) {
3659
- value[target] = value.type === "integer" ? bound + step : bound;
3702
+ if (typeof bound2 === "number" && value[target] === void 0) {
3703
+ value[target] = value.type === "integer" ? bound2 + step : bound2;
3660
3704
  }
3661
3705
  }
3662
3706
  for (const item of Object.values(value)) {
@@ -3750,7 +3794,7 @@ function buildRequestPlan(options, method) {
3750
3794
  throw new ProviderError("gemini", formatUnsupportedModelMessage(options.model));
3751
3795
  }
3752
3796
  const projectId = getGoogleProject(options);
3753
- const request = buildGenerateRequest(options);
3797
+ const request = mergeProviderOptions(buildGenerateRequest(options), options.providerOptions);
3754
3798
  return {
3755
3799
  url: getCodeAssistEndpoint(method),
3756
3800
  headers: {
@@ -3988,6 +4032,449 @@ function toError3(err) {
3988
4032
  return new ProviderError("gemini", String(err));
3989
4033
  }
3990
4034
 
4035
+ // src/redaction.ts
4036
+ var REDACTED = "[REDACTED]";
4037
+ var TRUNCATED = "[TRUNCATED]";
4038
+ var CIRCULAR = "[CIRCULAR]";
4039
+ var SENSITIVE_NAME = /(?:^|[_-])(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|key|auth(?:orization)?|bearer|cookie|credential|private[_-]?key|password|passwd|secret)(?:$|[_-])/i;
4040
+ 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;
4041
+ 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;
4042
+ var URL_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#@:"'`<>]*:[^\s/?#"'`<>]+@/gi;
4043
+ var URL_USERINFO_TO_FIRST_AT = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]*:[^\s/@]+@/gi;
4044
+ function escaped(value) {
4045
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4046
+ }
4047
+ function normalizedSecrets(secrets) {
4048
+ if (!secrets) return [];
4049
+ return [...new Set([...secrets].filter((value) => value.length >= 8 && value !== REDACTED))].sort(
4050
+ (a, b) => b.length - a.length
4051
+ );
4052
+ }
4053
+ function environmentSecrets(env) {
4054
+ const values = /* @__PURE__ */ new Set();
4055
+ for (const [name, value] of Object.entries(env)) {
4056
+ if (!value || value.length < 8 || value === REDACTED || !SENSITIVE_NAME.test(name)) continue;
4057
+ values.add(value);
4058
+ }
4059
+ return [...values].sort((a, b) => b.length - a.length);
4060
+ }
4061
+ function redactText(text, options = {}) {
4062
+ let result = text;
4063
+ result = result.replace(
4064
+ /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g,
4065
+ REDACTED
4066
+ );
4067
+ result = result.replace(URL_USERINFO, `$1${REDACTED}@`);
4068
+ result = result.replace(URL_USERINFO_TO_FIRST_AT, `$1${REDACTED}@`);
4069
+ result = result.replace(
4070
+ /\b(authorization\s*[:=]\s*)(?:bearer|basic)\s+[^\s,;]+/gi,
4071
+ `$1${REDACTED}`
4072
+ );
4073
+ result = result.replace(
4074
+ /\b(bearer|basic)\s+(?![A-Za-z][a-z]{0,14}\b)[A-Za-z0-9+/_.=-]{8,}/gi,
4075
+ `$1 ${REDACTED}`
4076
+ );
4077
+ result = result.replace(/\b(cookie|set-cookie)(\s*:\s*)[^\s=;:]+=[^\r\n]*/gi, `$1$2${REDACTED}`);
4078
+ result = result.replace(
4079
+ /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g,
4080
+ REDACTED
4081
+ );
4082
+ result = result.replace(
4083
+ /\b(?:sk-(?:ant-|proj-)?|xox[baprs]-|gh[pousr]_|github_pat_|AIza)[A-Za-z0-9_-]{12,}\b/g,
4084
+ REDACTED
4085
+ );
4086
+ result = result.replace(
4087
+ ENV_SECRET_ASSIGNMENT,
4088
+ (_match, name, separator, quote) => `${name}${separator}${quote}${REDACTED}${quote}`
4089
+ );
4090
+ result = result.replace(
4091
+ COMPACT_SECRET_ASSIGNMENT,
4092
+ (_match, name, quote) => `${name}=${quote}${REDACTED}${quote}`
4093
+ );
4094
+ for (const secret of normalizedSecrets(options.secrets)) {
4095
+ result = result.replace(new RegExp(escaped(secret), "g"), REDACTED);
4096
+ }
4097
+ const maxStringLength = options.maxStringLength ?? 1e6;
4098
+ if (result.length > maxStringLength) {
4099
+ result = `${result.slice(0, maxStringLength)}${TRUNCATED}`;
4100
+ }
4101
+ return result;
4102
+ }
4103
+ function isBinary(value) {
4104
+ return value instanceof ArrayBuffer || ArrayBuffer.isView(value) || typeof Blob !== "undefined" && value instanceof Blob;
4105
+ }
4106
+ function isMediaObject(value) {
4107
+ return (value.type === "image" || value.type === "video") && (typeof value.data === "string" || typeof value.url === "string");
4108
+ }
4109
+ function redactValue(value, options = {}) {
4110
+ const maxDepth = options.maxDepth ?? 20;
4111
+ const maxEntries = options.maxEntries ?? 1e4;
4112
+ const ancestors = /* @__PURE__ */ new WeakSet();
4113
+ let entries = 0;
4114
+ const visit = (current, depth, sensitive = false) => {
4115
+ if (typeof current === "string") {
4116
+ if (sensitive && current.length > 0 && current !== REDACTED) return REDACTED;
4117
+ return redactText(current, options);
4118
+ }
4119
+ if (current === null || current === void 0 || typeof current === "number" || typeof current === "boolean" || typeof current === "bigint") {
4120
+ return current;
4121
+ }
4122
+ if (typeof current !== "object") return current;
4123
+ if (isBinary(current)) return current;
4124
+ if (current instanceof Date) return new Date(current.getTime());
4125
+ if (depth >= maxDepth) return TRUNCATED;
4126
+ if (ancestors.has(current)) return CIRCULAR;
4127
+ ancestors.add(current);
4128
+ try {
4129
+ return cloneObject(current, depth);
4130
+ } finally {
4131
+ ancestors.delete(current);
4132
+ }
4133
+ };
4134
+ const cloneObject = (current, depth) => {
4135
+ if (current instanceof Error) {
4136
+ const error = {
4137
+ name: current.name,
4138
+ message: visit(current.message, depth + 1),
4139
+ stack: visit(current.stack, depth + 1)
4140
+ };
4141
+ for (const [key, child] of Object.entries(current)) {
4142
+ error[key] = visit(child, depth + 1, SENSITIVE_NAME.test(key));
4143
+ }
4144
+ return error;
4145
+ }
4146
+ if (Array.isArray(current)) {
4147
+ const clone2 = [];
4148
+ for (const child of current) {
4149
+ if (++entries > maxEntries) {
4150
+ clone2.push(TRUNCATED);
4151
+ break;
4152
+ }
4153
+ clone2.push(visit(child, depth + 1));
4154
+ }
4155
+ return clone2;
4156
+ }
4157
+ const record = current;
4158
+ if (isMediaObject(record)) return { ...record };
4159
+ const clone = {};
4160
+ for (const [key, child] of Object.entries(record)) {
4161
+ if (++entries > maxEntries) {
4162
+ clone[TRUNCATED] = true;
4163
+ break;
4164
+ }
4165
+ clone[key] = visit(child, depth + 1, SENSITIVE_NAME.test(key));
4166
+ }
4167
+ return clone;
4168
+ };
4169
+ return visit(value, 0);
4170
+ }
4171
+
4172
+ // src/utils/json-post.ts
4173
+ var MAX_ERROR_MESSAGE_CHARS = 240;
4174
+ function abortError() {
4175
+ return new DOMException("Aborted", "AbortError");
4176
+ }
4177
+ function throwIfAborted(signal) {
4178
+ if (signal?.aborted) throw abortError();
4179
+ }
4180
+ async function postJson(request) {
4181
+ const { provider, signal } = request;
4182
+ const secrets = request.secrets?.filter((s) => typeof s === "string" && s !== "");
4183
+ const clean = (text2) => bound(redactText(text2, { secrets }));
4184
+ const fetchImpl = request.fetch ?? globalThis.fetch;
4185
+ throwIfAborted(signal);
4186
+ let response;
4187
+ try {
4188
+ response = await fetchImpl(request.url, {
4189
+ method: "POST",
4190
+ headers: request.headers,
4191
+ body: JSON.stringify(request.body),
4192
+ signal
4193
+ });
4194
+ } catch (err) {
4195
+ if (signal?.aborted || isAbortError2(err)) throw abortError();
4196
+ const detail = err instanceof Error ? err.message : String(err);
4197
+ throw new GGAIError(`Couldn't reach ${provider}: ${clean(detail)}`, {
4198
+ source: "network",
4199
+ cause: err
4200
+ });
4201
+ }
4202
+ const headerRequestId = readHeader(
4203
+ response.headers,
4204
+ "x-request-id",
4205
+ "request-id",
4206
+ "openai-request-id"
4207
+ );
4208
+ let text;
4209
+ try {
4210
+ text = await response.text();
4211
+ } catch (err) {
4212
+ if (signal?.aborted || isAbortError2(err)) throw abortError();
4213
+ throw new GGAIError(`Lost the connection to ${provider} while reading its response.`, {
4214
+ source: "network",
4215
+ cause: err
4216
+ });
4217
+ }
4218
+ if (!response.ok) {
4219
+ throw httpError(provider, response, text, headerRequestId, clean);
4220
+ }
4221
+ try {
4222
+ return {
4223
+ json: JSON.parse(text),
4224
+ ...headerRequestId ? { requestId: headerRequestId } : {}
4225
+ };
4226
+ } catch {
4227
+ throw new ProviderError(provider, `${provider} returned a response that isn't valid JSON.`, {
4228
+ statusCode: response.status,
4229
+ ...headerRequestId ? { requestId: headerRequestId } : {}
4230
+ });
4231
+ }
4232
+ }
4233
+ function httpError(provider, response, text, headerRequestId, clean) {
4234
+ const status = response.status;
4235
+ const parsed = parseErrorBody(text);
4236
+ const rawMessage = parsed.message ?? (parsed.isJson ? "" : text.trim());
4237
+ const message = !rawMessage ? emptyProviderErrorMessage(status) : isRawHtmlErrorEcho(rawMessage) ? providerHtmlErrorMessage(status) : clean(rawMessage);
4238
+ const requestId = headerRequestId ?? parsed.requestId;
4239
+ const base = {
4240
+ statusCode: status,
4241
+ ...requestId ? { requestId } : {}
4242
+ };
4243
+ if (status === 401 || status === 403) {
4244
+ return new ProviderError(provider, message, { ...base, source: "auth" });
4245
+ }
4246
+ const codeType = `${parsed.code ?? ""} ${parsed.type ?? ""}`.toLowerCase();
4247
+ if (status === 402 || codeType.includes("insufficient_quota") || isHardBillingMessage(message)) {
4248
+ const stamped = /usage limit reached/i.test(message) ? message : `usage limit reached: ${message}`;
4249
+ return new ProviderError(provider, stamped, base);
4250
+ }
4251
+ if (status === 429) {
4252
+ const retryAfter = Number(readHeader(response.headers, "retry-after"));
4253
+ const resetsAt = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.floor(Date.now() / 1e3) + Math.ceil(retryAfter) : void 0;
4254
+ return new ProviderError(provider, message, {
4255
+ ...base,
4256
+ ...resetsAt !== void 0 ? { resetsAt } : {}
4257
+ });
4258
+ }
4259
+ return new ProviderError(provider, message, base);
4260
+ }
4261
+ function parseErrorBody(text) {
4262
+ let parsed;
4263
+ try {
4264
+ parsed = JSON.parse(text);
4265
+ } catch {
4266
+ return { isJson: false };
4267
+ }
4268
+ if (!isRecord(parsed)) return { isJson: true };
4269
+ const error = parsed.error;
4270
+ const errorObj = isRecord(error) ? error : void 0;
4271
+ const message = stringField(errorObj, "message") ?? (typeof error === "string" && error.trim() ? error.trim() : void 0) ?? stringField(parsed, "message") ?? stringField(parsed, "detail");
4272
+ const requestId = stringField(parsed, "request_id") ?? stringField(errorObj, "request_id");
4273
+ const code = stringField(errorObj, "code") ?? stringField(errorObj, "status");
4274
+ const type = stringField(errorObj, "type");
4275
+ return {
4276
+ isJson: true,
4277
+ ...message ? { message } : {},
4278
+ ...requestId ? { requestId } : {},
4279
+ ...code ? { code } : {},
4280
+ ...type ? { type } : {}
4281
+ };
4282
+ }
4283
+ function stringField(record, key) {
4284
+ const value = record?.[key];
4285
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
4286
+ }
4287
+ function isRecord(value) {
4288
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4289
+ }
4290
+ function isAbortError2(err) {
4291
+ return (err instanceof Error || err instanceof DOMException) && err.name === "AbortError";
4292
+ }
4293
+ function bound(text) {
4294
+ const trimmed = text.trim();
4295
+ return trimmed.length > MAX_ERROR_MESSAGE_CHARS ? `${trimmed.slice(0, MAX_ERROR_MESSAGE_CHARS)}\u2026` : trimmed;
4296
+ }
4297
+ function joinUrl(baseUrl, path) {
4298
+ return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
4299
+ }
4300
+
4301
+ // src/providers/gemini-embeddings.ts
4302
+ var GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
4303
+ var GEMINI_EMBED_BATCH_SIZE = 100;
4304
+ var TASK_TYPES = {
4305
+ query: "RETRIEVAL_QUERY",
4306
+ document: "RETRIEVAL_DOCUMENT"
4307
+ };
4308
+ function supportsTaskType(model) {
4309
+ return !/^gemini-embedding-2(?:$|[-.@])/.test(model);
4310
+ }
4311
+ async function embedGemini(request) {
4312
+ if (!request.apiKey) {
4313
+ throw new GGAIError("Gemini embeddings need a Gemini API key (apiKey).", {
4314
+ source: "auth",
4315
+ hint: "Create a key in Google AI Studio. Gemini Code Assist sign-in can't be used for embeddings."
4316
+ });
4317
+ }
4318
+ const model = request.model.replace(/^models\//, "");
4319
+ const resource = `models/${model}`;
4320
+ const taskType = request.inputType && supportsTaskType(model) ? TASK_TYPES[request.inputType] : void 0;
4321
+ const embedContentConfig = {
4322
+ ...taskType ? { taskType } : {},
4323
+ ...request.dimensions !== void 0 ? { outputDimensionality: request.dimensions } : {}
4324
+ };
4325
+ const body = mergeProviderOptions(
4326
+ {
4327
+ requests: request.input.map((text) => ({
4328
+ model: resource,
4329
+ content: { parts: [{ text }] },
4330
+ ...Object.keys(embedContentConfig).length > 0 ? { embedContentConfig } : {}
4331
+ }))
4332
+ },
4333
+ request.providerOptions
4334
+ );
4335
+ const { json, requestId } = await postJson({
4336
+ provider: "gemini",
4337
+ url: joinUrl(request.baseUrl ?? GEMINI_API_BASE_URL, `${resource}:batchEmbedContents`),
4338
+ headers: { "Content-Type": "application/json", "x-goog-api-key": request.apiKey },
4339
+ body,
4340
+ fetch: request.fetch,
4341
+ signal: request.signal,
4342
+ secrets: [request.apiKey]
4343
+ });
4344
+ const invalid = (detail) => new ProviderError("gemini", `Invalid embeddings response: ${detail}.`, {
4345
+ ...requestId ? { requestId } : {}
4346
+ });
4347
+ if (!isRecord(json) || !Array.isArray(json.embeddings)) {
4348
+ throw invalid("missing `embeddings` array");
4349
+ }
4350
+ const embeddings = json.embeddings.map((entry) => {
4351
+ if (!isRecord(entry) || !Array.isArray(entry.values)) {
4352
+ throw invalid("an entry has no `values` array");
4353
+ }
4354
+ return entry.values;
4355
+ });
4356
+ const usage = json.usageMetadata;
4357
+ const promptTokens = isRecord(usage) ? usage.promptTokenCount : void 0;
4358
+ return {
4359
+ embeddings,
4360
+ model,
4361
+ ...typeof promptTokens === "number" && Number.isFinite(promptTokens) ? { usage: { inputTokens: promptTokens } } : {}
4362
+ };
4363
+ }
4364
+
4365
+ // src/providers/openai-compat-retrieval.ts
4366
+ function jsonHeaders(apiKey) {
4367
+ return {
4368
+ "Content-Type": "application/json",
4369
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
4370
+ };
4371
+ }
4372
+ async function embedOpenAICompatible(request, config) {
4373
+ const body = mergeProviderOptions(
4374
+ {
4375
+ model: config.wireModel ?? request.model,
4376
+ input: request.input,
4377
+ // gg-ai parses float arrays only; never let a server default to base64.
4378
+ encoding_format: "float",
4379
+ ...request.dimensions !== void 0 ? { dimensions: request.dimensions } : {},
4380
+ ...request.inputType && config.inputTypeFields ? config.inputTypeFields(request.inputType) : {}
4381
+ },
4382
+ request.providerOptions
4383
+ );
4384
+ const { json, requestId } = await postJson({
4385
+ provider: config.provider,
4386
+ url: joinUrl(config.baseUrl, "embeddings"),
4387
+ headers: jsonHeaders(request.apiKey),
4388
+ body,
4389
+ fetch: request.fetch,
4390
+ signal: request.signal,
4391
+ secrets: [request.apiKey]
4392
+ });
4393
+ const invalid = (detail) => new ProviderError(config.provider, `Invalid embeddings response: ${detail}.`, {
4394
+ ...requestId ? { requestId } : {}
4395
+ });
4396
+ if (!isRecord(json) || !Array.isArray(json.data)) throw invalid("missing `data` array");
4397
+ const items = json.data.map((item) => {
4398
+ if (!isRecord(item)) throw invalid("an entry in `data` is not an object");
4399
+ const index = item.index;
4400
+ if (index !== void 0 && !(typeof index === "number" && Number.isInteger(index))) {
4401
+ throw invalid("an entry has a non-integer `index`");
4402
+ }
4403
+ if (!Array.isArray(item.embedding)) {
4404
+ throw invalid("an entry's `embedding` is not an array of numbers");
4405
+ }
4406
+ return { index, embedding: item.embedding };
4407
+ });
4408
+ const withIndex = items.filter((item) => item.index !== void 0).length;
4409
+ let embeddings;
4410
+ if (withIndex === 0) {
4411
+ embeddings = items.map((item) => item.embedding);
4412
+ } else if (withIndex !== items.length) {
4413
+ throw invalid("some entries have an `index` and some don't");
4414
+ } else {
4415
+ const slots = new Array(items.length);
4416
+ for (const item of items) {
4417
+ const index = item.index;
4418
+ if (index < 0 || index >= items.length || slots[index] !== void 0) {
4419
+ throw invalid(`unexpected or duplicate index ${index}`);
4420
+ }
4421
+ slots[index] = item.embedding;
4422
+ }
4423
+ embeddings = slots;
4424
+ }
4425
+ const usage = json.usage;
4426
+ const promptTokens = isRecord(usage) ? usage.prompt_tokens : void 0;
4427
+ return {
4428
+ embeddings,
4429
+ ...typeof json.model === "string" && json.model ? { model: json.model } : {},
4430
+ ...typeof promptTokens === "number" && Number.isFinite(promptTokens) ? { usage: { inputTokens: promptTokens } } : {}
4431
+ };
4432
+ }
4433
+ async function rerankCohereCompatible(request, config) {
4434
+ const body = mergeProviderOptions(
4435
+ {
4436
+ model: config.wireModel ?? request.model,
4437
+ query: request.query,
4438
+ documents: request.documents,
4439
+ ...request.topN !== void 0 ? { top_n: request.topN } : {}
4440
+ },
4441
+ request.providerOptions
4442
+ );
4443
+ const { json, requestId } = await postJson({
4444
+ provider: config.provider,
4445
+ url: joinUrl(config.baseUrl, "rerank"),
4446
+ headers: jsonHeaders(request.apiKey),
4447
+ body,
4448
+ fetch: request.fetch,
4449
+ signal: request.signal,
4450
+ secrets: [request.apiKey]
4451
+ });
4452
+ const invalid = (detail) => new ProviderError(config.provider, `Invalid rerank response: ${detail}.`, {
4453
+ ...requestId ? { requestId } : {}
4454
+ });
4455
+ if (!isRecord(json) || !Array.isArray(json.results)) throw invalid("missing `results` array");
4456
+ const results = json.results.map((item) => {
4457
+ if (!isRecord(item)) throw invalid("an entry in `results` is not an object");
4458
+ const { index, relevance_score: score } = item;
4459
+ if (typeof index !== "number" || typeof score !== "number") {
4460
+ throw invalid("an entry is missing a numeric `index` or `relevance_score`");
4461
+ }
4462
+ return { index, score };
4463
+ });
4464
+ const usage = json.usage;
4465
+ const tokens = isRecord(usage) ? usage.prompt_tokens ?? usage.total_tokens : void 0;
4466
+ const searchUnits = isRecord(usage) ? usage.search_units : void 0;
4467
+ const rerankUsage = {
4468
+ ...typeof tokens === "number" && Number.isFinite(tokens) ? { inputTokens: tokens } : {},
4469
+ ...typeof searchUnits === "number" && Number.isFinite(searchUnits) ? { searchUnits } : {}
4470
+ };
4471
+ return {
4472
+ results,
4473
+ ...typeof json.model === "string" && json.model ? { model: json.model } : {},
4474
+ ...Object.keys(rerankUsage).length > 0 ? { usage: rerankUsage } : {}
4475
+ };
4476
+ }
4477
+
3991
4478
  // src/provider-registry.ts
3992
4479
  var ProviderRegistryImpl = class {
3993
4480
  providers = /* @__PURE__ */ new Map();
@@ -4017,6 +4504,11 @@ var ProviderRegistryImpl = class {
4017
4504
  has(name) {
4018
4505
  return this.providers.has(name);
4019
4506
  }
4507
+ /** Whether a registered provider offers a capability (`false` for unknown providers). */
4508
+ supports(name, capability) {
4509
+ const entry = this.providers.get(name);
4510
+ return typeof entry?.[capability] === "function";
4511
+ }
4020
4512
  /** List all registered provider names. */
4021
4513
  list() {
4022
4514
  return [...this.providers.keys()];
@@ -4194,6 +4686,10 @@ function observePreparedContext(before, after, tools) {
4194
4686
  }
4195
4687
 
4196
4688
  // src/stream.ts
4689
+ var OPENAI_EMBED_BATCH_SIZE = 2048;
4690
+ var OPENROUTER_EMBED_BATCH_SIZE = 64;
4691
+ var LOCAL_EMBED_BATCH_SIZE = 64;
4692
+ var OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
4197
4693
  var GLM_CODING_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
4198
4694
  var KIMI_CODE_USER_AGENT = `kimi-code-cli/${process.env.KIMI_CODE_VERSION ?? "1.0.11"}`;
4199
4695
  var GROK_CLI_PROXY_HOST = "cli-chat-proxy.grok.com";
@@ -4214,10 +4710,20 @@ providerRegistry.register("openai", {
4214
4710
  return streamOpenAICodex(options);
4215
4711
  }
4216
4712
  return streamOpenAI(options);
4217
- }
4713
+ },
4714
+ // OpenAI API-key auth only; ChatGPT-subscription (Codex) tokens have no embeddings API.
4715
+ // OpenAI has no task/input-type parameter, so `inputType` is ignored.
4716
+ embed: (request) => embedOpenAICompatible(request, {
4717
+ provider: "openai",
4718
+ baseUrl: request.baseUrl ?? "https://api.openai.com/v1"
4719
+ }),
4720
+ embedBatchSize: OPENAI_EMBED_BATCH_SIZE
4218
4721
  });
4219
4722
  providerRegistry.register("gemini", {
4220
- stream: (options) => streamGemini(options)
4723
+ stream: (options) => streamGemini(options),
4724
+ // Native Gemini API with an AI Studio key — not the Code Assist OAuth endpoint above.
4725
+ embed: (request) => embedGemini(request),
4726
+ embedBatchSize: GEMINI_EMBED_BATCH_SIZE
4221
4727
  });
4222
4728
  providerRegistry.register("glm", {
4223
4729
  stream: (options) => streamOpenAI({
@@ -4241,7 +4747,19 @@ providerRegistry.register("deepseek", {
4241
4747
  providerRegistry.register("openrouter", {
4242
4748
  stream: (options) => streamOpenAI({
4243
4749
  ...options,
4244
- baseUrl: options.baseUrl ?? "https://openrouter.ai/api/v1"
4750
+ baseUrl: options.baseUrl ?? OPENROUTER_BASE_URL
4751
+ }),
4752
+ embed: (request) => embedOpenAICompatible(request, {
4753
+ provider: "openrouter",
4754
+ baseUrl: request.baseUrl ?? OPENROUTER_BASE_URL,
4755
+ inputTypeFields: (inputType) => ({
4756
+ input_type: inputType === "query" ? "search_query" : "search_document"
4757
+ })
4758
+ }),
4759
+ embedBatchSize: OPENROUTER_EMBED_BATCH_SIZE,
4760
+ rerank: (request) => rerankCohereCompatible(request, {
4761
+ provider: "openrouter",
4762
+ baseUrl: request.baseUrl ?? OPENROUTER_BASE_URL
4245
4763
  })
4246
4764
  });
4247
4765
  providerRegistry.register("huggingface", {
@@ -4311,19 +4829,36 @@ providerRegistry.register("local", {
4311
4829
  // vLLM). There is no default endpoint: the baseUrl comes from the endpoint
4312
4830
  // credential the discovery layer wrote, so a missing one is a wiring bug, not
4313
4831
  // 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
- }
4832
+ stream: (options) => streamOpenAI({
4833
+ ...options,
4834
+ baseUrl: requireLocalBaseUrl(options.baseUrl),
4835
+ model: localWireModelId(options.model),
4836
+ webSearch: false
4837
+ }),
4838
+ // Every supported local server exposes OpenAI-compatible `/v1/embeddings`
4839
+ // (the model must be an embedding model). None has an input-type parameter.
4840
+ embed: (request) => embedOpenAICompatible(request, {
4841
+ provider: "local",
4842
+ baseUrl: requireLocalBaseUrl(request.baseUrl),
4843
+ wireModel: localWireModelId(request.model)
4844
+ }),
4845
+ embedBatchSize: LOCAL_EMBED_BATCH_SIZE,
4846
+ // llama.cpp (`llama-server --rerank`) and vLLM serve `/v1/rerank`; Ollama and
4847
+ // LM Studio don't, and answer with a 404 ProviderError.
4848
+ rerank: (request) => rerankCohereCompatible(request, {
4849
+ provider: "local",
4850
+ baseUrl: requireLocalBaseUrl(request.baseUrl),
4851
+ wireModel: localWireModelId(request.model)
4852
+ })
4326
4853
  });
4854
+ function requireLocalBaseUrl(baseUrl) {
4855
+ if (!baseUrl) {
4856
+ throw new GGAIError(
4857
+ "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."
4858
+ );
4859
+ }
4860
+ return baseUrl;
4861
+ }
4327
4862
  function stream(options) {
4328
4863
  const entry = providerRegistry.get(options.provider);
4329
4864
  if (!entry) {
@@ -4382,6 +4917,121 @@ function messagesContainVideo(messages) {
4382
4917
  return false;
4383
4918
  }
4384
4919
 
4920
+ // src/embed.ts
4921
+ var CAPABILITY_LABEL = {
4922
+ embed: "embeddings",
4923
+ rerank: "reranking"
4924
+ };
4925
+ function resolveCapability(provider, capability) {
4926
+ const entry = providerRegistry.get(provider);
4927
+ if (!entry) {
4928
+ throw new GGAIError(
4929
+ `Unknown provider: "${provider}". Registered: ${providerRegistry.list().join(", ")}`
4930
+ );
4931
+ }
4932
+ const fn = entry[capability];
4933
+ if (typeof fn !== "function") {
4934
+ const label = CAPABILITY_LABEL[capability];
4935
+ const offering = providerRegistry.list().filter((name) => providerRegistry.supports(name, capability)).sort();
4936
+ throw new GGAIError(`Provider "${provider}" doesn't support ${label}.`, {
4937
+ source: "capability",
4938
+ hint: offering.length > 0 ? `Use a provider that offers ${label}: ${offering.join(", ")}.` : `No registered provider offers ${label}.`
4939
+ });
4940
+ }
4941
+ return fn;
4942
+ }
4943
+ function assertPositiveInteger(name, value) {
4944
+ if (value !== void 0 && !(Number.isInteger(value) && value > 0)) {
4945
+ throw new GGAIError(`${name} must be a positive integer (got ${String(value)}).`);
4946
+ }
4947
+ }
4948
+ async function embed(options) {
4949
+ const embedFn = resolveCapability(options.provider, "embed");
4950
+ if (!Array.isArray(options.input)) {
4951
+ throw new GGAIError("embed() input must be an array of strings.");
4952
+ }
4953
+ assertPositiveInteger("dimensions", options.dimensions);
4954
+ assertPositiveInteger("batchSize", options.batchSize);
4955
+ if (options.input.length === 0) return { embeddings: [], model: options.model };
4956
+ const entry = providerRegistry.get(options.provider);
4957
+ const batchSize = options.batchSize ?? entry?.embedBatchSize ?? options.input.length;
4958
+ const { normalize, batchSize: _batchSize, ...request } = options;
4959
+ const embeddings = [];
4960
+ let model = options.model;
4961
+ let inputTokens;
4962
+ let expectedLength = options.dimensions;
4963
+ for (let start = 0; start < options.input.length; start += batchSize) {
4964
+ throwIfAborted(options.signal);
4965
+ const batch = options.input.slice(start, start + batchSize);
4966
+ const response = await embedFn({ ...request, input: batch });
4967
+ const invalid = (detail) => new ProviderError(options.provider, `Invalid embeddings response: ${detail}.`);
4968
+ if (!Array.isArray(response.embeddings) || response.embeddings.length !== batch.length) {
4969
+ const count = Array.isArray(response.embeddings) ? response.embeddings.length : 0;
4970
+ throw invalid(`expected ${batch.length} embeddings, got ${count}`);
4971
+ }
4972
+ for (const vector of response.embeddings) {
4973
+ if (!Array.isArray(vector) || vector.length === 0) throw invalid("received an empty vector");
4974
+ if (!vector.every((value) => typeof value === "number" && Number.isFinite(value))) {
4975
+ throw invalid("received a vector with non-numeric values");
4976
+ }
4977
+ if (expectedLength === void 0) {
4978
+ expectedLength = vector.length;
4979
+ } else if (vector.length !== expectedLength) {
4980
+ throw invalid(
4981
+ options.dimensions !== void 0 ? `requested ${options.dimensions} dimensions, got a vector with ${vector.length}` : `vectors have different lengths (${expectedLength} and ${vector.length})`
4982
+ );
4983
+ }
4984
+ embeddings.push(normalize ? l2Normalize(vector) : vector);
4985
+ }
4986
+ if (response.model) model = response.model;
4987
+ if (response.usage) inputTokens = (inputTokens ?? 0) + response.usage.inputTokens;
4988
+ }
4989
+ return {
4990
+ embeddings,
4991
+ model,
4992
+ ...inputTokens !== void 0 ? { usage: { inputTokens } } : {}
4993
+ };
4994
+ }
4995
+ async function rerank(options) {
4996
+ const rerankFn = resolveCapability(options.provider, "rerank");
4997
+ if (!Array.isArray(options.documents)) {
4998
+ throw new GGAIError("rerank() documents must be an array of strings.");
4999
+ }
5000
+ assertPositiveInteger("topN", options.topN);
5001
+ if (options.documents.length === 0) return { results: [], model: options.model };
5002
+ throwIfAborted(options.signal);
5003
+ const response = await rerankFn(options);
5004
+ const invalid = (detail) => new ProviderError(options.provider, `Invalid rerank response: ${detail}.`);
5005
+ if (!Array.isArray(response.results)) throw invalid("missing results");
5006
+ const seen = /* @__PURE__ */ new Set();
5007
+ const results = response.results.map(({ index, score }) => {
5008
+ if (!Number.isInteger(index) || index < 0 || index >= options.documents.length) {
5009
+ throw invalid(
5010
+ `index ${String(index)} is out of range for ${options.documents.length} documents`
5011
+ );
5012
+ }
5013
+ if (seen.has(index)) throw invalid(`index ${index} appears more than once`);
5014
+ if (typeof score !== "number" || !Number.isFinite(score)) {
5015
+ throw invalid(`document ${index} has a non-numeric score`);
5016
+ }
5017
+ seen.add(index);
5018
+ return { index, score };
5019
+ });
5020
+ results.sort((a, b) => b.score - a.score || a.index - b.index);
5021
+ return {
5022
+ results: options.topN !== void 0 ? results.slice(0, options.topN) : results,
5023
+ model: response.model ?? options.model,
5024
+ ...response.usage ? { usage: response.usage } : {}
5025
+ };
5026
+ }
5027
+ function l2Normalize(vector) {
5028
+ let sumOfSquares = 0;
5029
+ for (const value of vector) sumOfSquares += value * value;
5030
+ if (sumOfSquares === 0) return [...vector];
5031
+ const norm = Math.sqrt(sumOfSquares);
5032
+ return vector.map((value) => value / norm);
5033
+ }
5034
+
4385
5035
  // src/chat-error.ts
4386
5036
  function causeCode(error) {
4387
5037
  if (!(error instanceof Error) || !error.cause || typeof error.cause !== "object") return "";
@@ -4623,143 +5273,6 @@ Original: ${message}`;
4623
5273
  return message;
4624
5274
  }
4625
5275
 
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
5276
  // src/providers/palsu.ts
4764
5277
  function palsuText(text) {
4765
5278
  return { role: "assistant", content: text ? [{ type: "text", text }] : [] };
@@ -4840,7 +5353,8 @@ function computeCacheUsage(current, previous) {
4840
5353
  function registerPalsuProvider(config) {
4841
5354
  const name = config?.name ?? "palsu";
4842
5355
  const responses = [];
4843
- const state = { callCount: 0 };
5356
+ const state = { callCount: 0, embedCallCount: 0, rerankCallCount: 0 };
5357
+ const embedDimensions = config?.embedDimensions ?? PALSU_DEFAULT_EMBED_DIMENSIONS;
4844
5358
  const defaultResponse = config?.defaultResponse ?? palsuText("");
4845
5359
  const enableCache = config?.promptCache ?? false;
4846
5360
  let lastMessagesSerialized = null;
@@ -4908,10 +5422,72 @@ function registerPalsuProvider(config) {
4908
5422
  return yield* simulateStream(message, stopReason, options.signal, cacheUsage);
4909
5423
  })();
4910
5424
  return new StreamResult(gen, options.signal);
5425
+ },
5426
+ async embed(request) {
5427
+ throwIfAborted(request.signal);
5428
+ state.embedCallCount++;
5429
+ const dimensions = request.dimensions ?? embedDimensions;
5430
+ return {
5431
+ embeddings: request.input.map((text) => palsuEmbedding(text, dimensions)),
5432
+ model: request.model,
5433
+ usage: { inputTokens: request.input.reduce((n, text) => n + tokenize(text).length, 0) }
5434
+ };
5435
+ },
5436
+ ...config?.embedBatchSize !== void 0 ? { embedBatchSize: config.embedBatchSize } : {},
5437
+ async rerank(request) {
5438
+ throwIfAborted(request.signal);
5439
+ state.rerankCallCount++;
5440
+ return {
5441
+ results: request.documents.map((document, index) => ({
5442
+ index,
5443
+ score: palsuRerankScore(request.query, document)
5444
+ })),
5445
+ model: request.model
5446
+ };
4911
5447
  }
4912
5448
  });
4913
5449
  return handle;
4914
5450
  }
5451
+ var PALSU_DEFAULT_EMBED_DIMENSIONS = 64;
5452
+ function tokenize(text) {
5453
+ return text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
5454
+ }
5455
+ function fnv1a(text) {
5456
+ let hash = 2166136261;
5457
+ for (let i = 0; i < text.length; i++) {
5458
+ hash ^= text.charCodeAt(i);
5459
+ hash = Math.imul(hash, 16777619);
5460
+ }
5461
+ return hash >>> 0;
5462
+ }
5463
+ function palsuEmbedding(text, dimensions = PALSU_DEFAULT_EMBED_DIMENSIONS) {
5464
+ const vector = new Array(dimensions).fill(0);
5465
+ const tokens = tokenize(text);
5466
+ if (tokens.length === 0) {
5467
+ vector[0] = 1;
5468
+ return vector;
5469
+ }
5470
+ for (const token of tokens) {
5471
+ const hash = fnv1a(token);
5472
+ const slot = hash % dimensions;
5473
+ vector[slot] = (vector[slot] ?? 0) + (hash >>> 31 === 0 ? 1 : -1);
5474
+ }
5475
+ const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
5476
+ if (norm === 0) {
5477
+ vector.fill(0);
5478
+ vector[0] = 1;
5479
+ return vector;
5480
+ }
5481
+ return vector.map((value) => value / norm);
5482
+ }
5483
+ function palsuRerankScore(query, document) {
5484
+ const queryTokens = new Set(tokenize(query));
5485
+ if (queryTokens.size === 0) return 0;
5486
+ const documentTokens = new Set(tokenize(document));
5487
+ let hits = 0;
5488
+ for (const token of queryTokens) if (documentTokens.has(token)) hits++;
5489
+ return hits / queryTokens.size;
5490
+ }
4915
5491
  export {
4916
5492
  EventStream,
4917
5493
  GGAIError,
@@ -4922,6 +5498,7 @@ export {
4922
5498
  clampProviderContextImages,
4923
5499
  classifyProviderError,
4924
5500
  dropInvalidToolCalls,
5501
+ embed,
4925
5502
  environmentSecrets,
4926
5503
  formatChatError,
4927
5504
  formatError,
@@ -4932,6 +5509,8 @@ export {
4932
5509
  isValidToolCallName,
4933
5510
  localWireModelId,
4934
5511
  palsuAssistantMessage,
5512
+ palsuEmbedding,
5513
+ palsuRerankScore,
4935
5514
  palsuText,
4936
5515
  palsuThinking,
4937
5516
  palsuToolCall,
@@ -4940,6 +5519,7 @@ export {
4940
5519
  redactText,
4941
5520
  redactValue,
4942
5521
  registerPalsuProvider,
5522
+ rerank,
4943
5523
  resolveToolSchema,
4944
5524
  sanitizeMessagesForWire,
4945
5525
  setProviderDiagnostic,