@backtest-kit/ollama 14.1.0 → 15.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.md +287 -287
  2. package/build/index.cjs +224 -160
  3. package/build/index.mjs +226 -162
  4. package/package.json +114 -112
  5. package/types.d.ts +16 -14
package/build/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { event, validateToolArguments, RoundRobin, addCompletion } from 'agent-swarm-kit';
2
2
  import { scoped } from 'di-scoped';
3
- import path, { join } from 'path';
3
+ import path, { join, isAbsolute } from 'path';
4
4
  import { createRequire } from 'module';
5
5
  import { createActivator } from 'di-kit';
6
- import { memoize, ToolRegistry, Subject, trycatch, errorData, getErrorMessage, iterateDocuments, distinctDocuments, resolveDocuments, singleshot, randomString, fetchApi, str, timeout } from 'functools-kit';
6
+ import { memoize, ToolRegistry, Subject, trycatch, errorData, getErrorMessage, iterateDocuments, distinctDocuments, resolveDocuments, randomString, fetchApi, singleshot, str, timeout } from 'functools-kit';
7
7
  import { MarkdownWriter, getSymbol, getContext, getMode } from 'backtest-kit';
8
8
  import MarkdownIt from 'markdown-it';
9
9
  import sanitizeHtml from 'sanitize-html';
@@ -294,7 +294,9 @@ const TYPES = {
294
294
  ...publicServices$1,
295
295
  };
296
296
 
297
- const PROMPT_TYPE_SYMBOL = Symbol("prompt-type");
297
+ // Symbol.for keeps the brand check working when CJS and ESM copies of the
298
+ // package end up in the same process (dual-package hazard).
299
+ const PROMPT_TYPE_SYMBOL = Symbol.for("backtest-kit.ollama.prompt-type");
298
300
  class Prompt {
299
301
  constructor(source) {
300
302
  this.source = source;
@@ -314,7 +316,7 @@ Prompt.isPrompt = (value) => {
314
316
  };
315
317
 
316
318
  const require = createRequire(import.meta.url);
317
- const REQUIRE_MODULE_FN = memoize(([module]) => module, (module) => {
319
+ const REQUIRE_MODULE_FN = memoize(([module]) => join(module.baseDir, module.path), (module) => {
318
320
  const modulePath = require.resolve(join(module.baseDir, module.path));
319
321
  return require(modulePath);
320
322
  });
@@ -334,7 +336,8 @@ class PromptCacheService {
334
336
  module,
335
337
  });
336
338
  if (module) {
337
- REQUIRE_MODULE_FN.clear(module);
339
+ REQUIRE_MODULE_FN.clear(join(module.baseDir, module.path));
340
+ return;
338
341
  }
339
342
  REQUIRE_MODULE_FN.clear();
340
343
  };
@@ -895,6 +898,13 @@ const toPlainString = async (content) => {
895
898
  typographer: true,
896
899
  });
897
900
  let telegramHtml = md.render(markdown);
901
+ // sanitize-html transformTags cannot insert text content, so list markers
902
+ // are injected into the HTML as text before sanitizing.
903
+ telegramHtml = telegramHtml.replace(/<ol[^>]*>[\s\S]*?<\/ol>/g, (block) => {
904
+ let counter = 0;
905
+ return block.replace(/<li>/g, () => `<li>${++counter}. `);
906
+ });
907
+ telegramHtml = telegramHtml.replace(/<li>(?!\d+\. )/g, "<li>• ");
898
908
  telegramHtml = sanitizeHtml(telegramHtml, {
899
909
  allowedTags: [
900
910
  "b",
@@ -923,7 +933,7 @@ const toPlainString = async (content) => {
923
933
  em: "",
924
934
  p: () => "",
925
935
  ul: () => "",
926
- li: () => "",
936
+ li: () => "",
927
937
  ol: () => "",
928
938
  hr: () => "\n",
929
939
  br: () => "\n",
@@ -1192,6 +1202,10 @@ class OptimizerTemplateService {
1192
1202
  ``,
1193
1203
  ` await dumpJson(resultId, messages, result);`,
1194
1204
  ``,
1205
+ ` if (result.position === "wait") {`,
1206
+ ` return null;`,
1207
+ ` }`,
1208
+ ``,
1195
1209
  ` result.id = resultId;`,
1196
1210
  ``,
1197
1211
  ` return result;`,
@@ -1878,8 +1892,9 @@ const GET_STRATEGY_DATA_FN = async (symbol, self) => {
1878
1892
  const strategyList = [];
1879
1893
  const totalSources = self.params.rangeTrain.length * self.params.source.length;
1880
1894
  let processedSources = 0;
1881
- for (const { startDate, endDate } of self.params.rangeTrain) {
1895
+ for (const { note, startDate, endDate } of self.params.rangeTrain) {
1882
1896
  const messageList = [];
1897
+ const sourceNames = [];
1883
1898
  for (const source of self.params.source) {
1884
1899
  // Emit progress event at the start of processing each source
1885
1900
  await self.onProgress({
@@ -1907,6 +1922,7 @@ const GET_STRATEGY_DATA_FN = async (symbol, self) => {
1907
1922
  role: "assistant",
1908
1923
  content: assistantContent,
1909
1924
  });
1925
+ sourceNames.push(DEFAULT_SOURCE_NAME);
1910
1926
  processedSources++;
1911
1927
  }
1912
1928
  else {
@@ -1928,18 +1944,19 @@ const GET_STRATEGY_DATA_FN = async (symbol, self) => {
1928
1944
  role: "assistant",
1929
1945
  content: assistantContent,
1930
1946
  });
1947
+ sourceNames.push(name || DEFAULT_SOURCE_NAME);
1931
1948
  processedSources++;
1932
1949
  }
1933
- const name = "name" in source
1934
- ? source.name || DEFAULT_SOURCE_NAME
1935
- : DEFAULT_SOURCE_NAME;
1936
- strategyList.push({
1937
- symbol,
1938
- name,
1939
- messages: messageList,
1940
- strategy: await self.params.getPrompt(symbol, messageList),
1941
- });
1942
1950
  }
1951
+ // One strategy per training range: getPrompt receives the complete
1952
+ // conversation history with all sources (see IOptimizerSchema.getPrompt).
1953
+ // Messages are snapshotted so later ranges cannot mutate this strategy.
1954
+ strategyList.push({
1955
+ symbol,
1956
+ name: note || sourceNames.join("+") || DEFAULT_SOURCE_NAME,
1957
+ messages: [...messageList],
1958
+ strategy: await self.params.getPrompt(symbol, messageList),
1959
+ });
1943
1960
  }
1944
1961
  // Emit final progress event (100%)
1945
1962
  await self.onProgress({
@@ -2045,7 +2062,7 @@ const GET_STRATEGY_CODE_FN = async (symbol, self) => {
2045
2062
  const GET_STRATEGY_DUMP_FN = async (symbol, path, self) => {
2046
2063
  const report = await self.getCode(symbol);
2047
2064
  try {
2048
- const dir = join(process.cwd(), path);
2065
+ const dir = isAbsolute(path) ? path : join(process.cwd(), path);
2049
2066
  await mkdir(dir, { recursive: true });
2050
2067
  const filename = `${self.params.optimizerName}_${symbol}.mjs`;
2051
2068
  const filepath = join(dir, filename);
@@ -2435,17 +2452,17 @@ var InferenceName$1 = InferenceName;
2435
2452
  * });
2436
2453
  * ```
2437
2454
  */
2438
- const getGrok = singleshot(() => {
2455
+ const GET_CLIENT_FN$9 = memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
2456
+ baseURL: "https://api.x.ai/v1",
2457
+ apiKey,
2458
+ }));
2459
+ const getGrok = () => {
2439
2460
  const apiKey = engine$1.contextService.context.apiKey;
2440
2461
  if (Array.isArray(apiKey)) {
2441
- getGrok.clear();
2442
2462
  throw new Error("Grok provider does not support token rotation");
2443
2463
  }
2444
- return new OpenAI({
2445
- baseURL: "https://api.x.ai/v1",
2446
- apiKey: apiKey,
2447
- });
2448
- });
2464
+ return GET_CLIENT_FN$9(apiKey);
2465
+ };
2449
2466
 
2450
2467
  /**
2451
2468
  * Global configuration parameters for the Ollama package.
@@ -2581,7 +2598,7 @@ class GrokProvider {
2581
2598
  },
2582
2599
  });
2583
2600
  const result = {
2584
- content: content,
2601
+ content: content ?? "",
2585
2602
  mode,
2586
2603
  agentName,
2587
2604
  role,
@@ -2741,8 +2758,8 @@ class GrokProvider {
2741
2758
  Authorization: `Bearer ${this.contextService.context.apiKey}`,
2742
2759
  },
2743
2760
  body: JSON.stringify({
2761
+ model: this.contextService.context.model,
2744
2762
  messages,
2745
- context: this.contextService.context,
2746
2763
  max_tokens: 5000,
2747
2764
  response_format: format,
2748
2765
  }),
@@ -3128,7 +3145,12 @@ class HfProvider {
3128
3145
  continue;
3129
3146
  }
3130
3147
  set(validation.data, "_thinking", reasoning_content);
3131
- set(validation.data, "_context", this.contextService.context);
3148
+ {
3149
+ // apiKey must never reach the outline result: it gets dumped to
3150
+ // markdown files and forwarded to downstream consumers.
3151
+ const { inference, model } = this.contextService.context;
3152
+ set(validation.data, "_context", { inference, model });
3153
+ }
3132
3154
  const result = {
3133
3155
  role: "assistant",
3134
3156
  content: JSON.stringify(validation.data),
@@ -3163,10 +3185,12 @@ class HfProvider {
3163
3185
  * @throws Error if no API keys are provided in context
3164
3186
  */
3165
3187
  class OllamaWrapper {
3166
- constructor(_config) {
3188
+ constructor(_config, apiKeys) {
3167
3189
  this._config = _config;
3168
- /** Round-robin chat function factory */
3169
- this._chatFn = RoundRobin.create(engine$1.contextService.context.apiKey, (token) => {
3190
+ if (!apiKeys?.length) {
3191
+ throw new Error("OllamaRotate required apiKey[] to process token rotation");
3192
+ }
3193
+ this._chatFn = RoundRobin.create(apiKeys, (token) => {
3170
3194
  const ollama = new Ollama({
3171
3195
  ...this._config,
3172
3196
  headers: {
@@ -3182,9 +3206,6 @@ class OllamaWrapper {
3182
3206
  }
3183
3207
  };
3184
3208
  });
3185
- if (!engine$1.contextService.context.apiKey) {
3186
- throw new Error("OllamaRotate required apiKey[] to process token rotation");
3187
- }
3188
3209
  }
3189
3210
  /**
3190
3211
  * Executes a chat request with automatic token rotation.
@@ -3217,10 +3238,28 @@ class OllamaWrapper {
3217
3238
  * // Next request will use a different API key
3218
3239
  * ```
3219
3240
  */
3220
- const getOllamaRotate = singleshot(() => new OllamaWrapper({
3241
+ const GET_WRAPPER_FN = memoize(([apiKeys]) => apiKeys.join(","), (apiKeys) => new OllamaWrapper({
3221
3242
  host: "https://ollama.com",
3222
- }));
3243
+ }, apiKeys));
3244
+ const getOllamaRotate = () => {
3245
+ const apiKey = engine$1.contextService.context.apiKey;
3246
+ if (!Array.isArray(apiKey)) {
3247
+ throw new Error("OllamaRotate required apiKey[] to process token rotation");
3248
+ }
3249
+ return GET_WRAPPER_FN(apiKey);
3250
+ };
3223
3251
 
3252
+ const GET_CLIENT_FN$8 = memoize(([apiKey]) => `${apiKey ?? ""}`, (apiKey) => {
3253
+ if (!apiKey) {
3254
+ return new Ollama();
3255
+ }
3256
+ return new Ollama({
3257
+ host: "https://ollama.com",
3258
+ headers: {
3259
+ Authorization: `Bearer ${apiKey}`,
3260
+ },
3261
+ });
3262
+ });
3224
3263
  /**
3225
3264
  * Creates and caches an Ollama client with flexible configuration.
3226
3265
  *
@@ -3266,21 +3305,13 @@ const getOllamaRotate = singleshot(() => new OllamaWrapper({
3266
3305
  * });
3267
3306
  * ```
3268
3307
  */
3269
- const getOllama = singleshot(() => {
3308
+ const getOllama = () => {
3270
3309
  const apiKey = engine$1.contextService.context.apiKey;
3271
3310
  if (Array.isArray(apiKey)) {
3272
3311
  return getOllamaRotate();
3273
3312
  }
3274
- if (!apiKey) {
3275
- return new Ollama();
3276
- }
3277
- return new Ollama({
3278
- host: "https://ollama.com",
3279
- headers: {
3280
- Authorization: `Bearer ${apiKey}`,
3281
- },
3282
- });
3283
- });
3313
+ return GET_CLIENT_FN$8(apiKey);
3314
+ };
3284
3315
 
3285
3316
  /**
3286
3317
  * Maximum number of retry attempts for outline completion when model fails to use tools correctly.
@@ -3614,7 +3645,12 @@ class OllamaProvider {
3614
3645
  attempt++;
3615
3646
  continue;
3616
3647
  }
3617
- set(validation.data, "_context", this.contextService.context);
3648
+ {
3649
+ // apiKey must never reach the outline result: it gets dumped to
3650
+ // markdown files and forwarded to downstream consumers.
3651
+ const { inference, model } = this.contextService.context;
3652
+ set(validation.data, "_context", { inference, model });
3653
+ }
3618
3654
  const result = {
3619
3655
  role: "assistant",
3620
3656
  content: JSON.stringify(validation.data),
@@ -3633,6 +3669,10 @@ class OllamaProvider {
3633
3669
  }
3634
3670
  }
3635
3671
 
3672
+ const GET_CLIENT_FN$7 = memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
3673
+ baseURL: "https://api.anthropic.com/v1/",
3674
+ apiKey,
3675
+ }));
3636
3676
  /**
3637
3677
  * Creates and caches an OpenAI-compatible client for Claude (Anthropic) API.
3638
3678
  *
@@ -3660,17 +3700,13 @@ class OllamaProvider {
3660
3700
  * });
3661
3701
  * ```
3662
3702
  */
3663
- const getClaude = singleshot(() => {
3703
+ const getClaude = () => {
3664
3704
  const apiKey = engine$1.contextService.context.apiKey;
3665
3705
  if (Array.isArray(apiKey)) {
3666
- getClaude.clear();
3667
3706
  throw new Error("Claude provider does not support token rotation");
3668
3707
  }
3669
- return new OpenAI({
3670
- baseURL: "https://api.anthropic.com/v1/",
3671
- apiKey,
3672
- });
3673
- });
3708
+ return GET_CLIENT_FN$7(apiKey);
3709
+ };
3674
3710
 
3675
3711
  /**
3676
3712
  * Maximum number of retry attempts for outline completion when model fails to use tools correctly.
@@ -3773,7 +3809,7 @@ class ClaudeProvider {
3773
3809
  }
3774
3810
  const { choices: [{ message: { content, role, tool_calls }, },], } = await claude.chat.completions.create(requestOptions);
3775
3811
  const result = {
3776
- content: content,
3812
+ content: content ?? "",
3777
3813
  mode,
3778
3814
  agentName,
3779
3815
  role,
@@ -3854,7 +3890,7 @@ class ClaudeProvider {
3854
3890
  };
3855
3891
  // Debug logging
3856
3892
  if (GLOBAL_CONFIG.CC_ENABLE_DEBUG) {
3857
- await fs.appendFile("./debug_gpt5_provider_stream.txt", JSON.stringify({ params, answer: result }, null, 2) + "\n\n");
3893
+ await fs.appendFile("./debug_claude_provider_stream.txt", JSON.stringify({ params, answer: result }, null, 2) + "\n\n");
3858
3894
  }
3859
3895
  return result;
3860
3896
  }
@@ -3957,7 +3993,12 @@ class ClaudeProvider {
3957
3993
  attempt++;
3958
3994
  continue;
3959
3995
  }
3960
- set(validation.data, "_context", this.contextService.context);
3996
+ {
3997
+ // apiKey must never reach the outline result: it gets dumped to
3998
+ // markdown files and forwarded to downstream consumers.
3999
+ const { inference, model } = this.contextService.context;
4000
+ set(validation.data, "_context", { inference, model });
4001
+ }
3961
4002
  const result = {
3962
4003
  role: "assistant",
3963
4004
  content: JSON.stringify(validation.data),
@@ -3976,6 +4017,9 @@ class ClaudeProvider {
3976
4017
  }
3977
4018
  }
3978
4019
 
4020
+ const GET_CLIENT_FN$6 = memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
4021
+ apiKey,
4022
+ }));
3979
4023
  /**
3980
4024
  * Creates and caches an OpenAI client for OpenAI API.
3981
4025
  *
@@ -4003,16 +4047,13 @@ class ClaudeProvider {
4003
4047
  * });
4004
4048
  * ```
4005
4049
  */
4006
- const getOpenAi = singleshot(() => {
4050
+ const getOpenAi = () => {
4007
4051
  const apiKey = engine$1.contextService.context.apiKey;
4008
4052
  if (Array.isArray(apiKey)) {
4009
- getOpenAi.clear();
4010
4053
  throw new Error("OpenAI provider does not support token rotation");
4011
4054
  }
4012
- return new OpenAI({
4013
- apiKey: apiKey,
4014
- });
4015
- });
4055
+ return GET_CLIENT_FN$6(apiKey);
4056
+ };
4016
4057
 
4017
4058
  /**
4018
4059
  * Provider for OpenAI GPT models (GPT-4, GPT-4 Turbo, GPT-3.5, etc.).
@@ -4129,7 +4170,7 @@ class GPT5Provider {
4129
4170
  tools: tools,
4130
4171
  });
4131
4172
  const result = {
4132
- content: content,
4173
+ content: content ?? "",
4133
4174
  mode,
4134
4175
  agentName,
4135
4176
  role,
@@ -4354,17 +4395,17 @@ class GPT5Provider {
4354
4395
  * });
4355
4396
  * ```
4356
4397
  */
4357
- const getDeepseek = singleshot(() => {
4398
+ const GET_CLIENT_FN$5 = memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
4399
+ baseURL: "https://api.deepseek.com",
4400
+ apiKey,
4401
+ }));
4402
+ const getDeepseek = () => {
4358
4403
  const apiKey = engine$1.contextService.context.apiKey;
4359
4404
  if (Array.isArray(apiKey)) {
4360
- getDeepseek.clear();
4361
4405
  throw new Error("Deepseek provider does not support token rotation");
4362
4406
  }
4363
- return new OpenAI({
4364
- baseURL: "https://api.deepseek.com",
4365
- apiKey: apiKey,
4366
- });
4367
- });
4407
+ return GET_CLIENT_FN$5(apiKey);
4408
+ };
4368
4409
 
4369
4410
  /**
4370
4411
  * Maximum number of retry attempts for outline completion.
@@ -4447,7 +4488,7 @@ class DeepseekProvider {
4447
4488
  tools: formattedTools?.length ? formattedTools : undefined,
4448
4489
  });
4449
4490
  const result = {
4450
- content: content,
4491
+ content: content ?? "",
4451
4492
  mode,
4452
4493
  agentName,
4453
4494
  role,
@@ -4630,7 +4671,12 @@ class DeepseekProvider {
4630
4671
  attempt++;
4631
4672
  continue;
4632
4673
  }
4633
- set(validation.data, "_context", this.contextService.context);
4674
+ {
4675
+ // apiKey must never reach the outline result: it gets dumped to
4676
+ // markdown files and forwarded to downstream consumers.
4677
+ const { inference, model } = this.contextService.context;
4678
+ set(validation.data, "_context", { inference, model });
4679
+ }
4634
4680
  const result = {
4635
4681
  role: "assistant",
4636
4682
  content: JSON.stringify(validation.data),
@@ -4676,17 +4722,17 @@ class DeepseekProvider {
4676
4722
  * });
4677
4723
  * ```
4678
4724
  */
4679
- const getMistral = singleshot(() => {
4725
+ const GET_CLIENT_FN$4 = memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
4726
+ baseURL: "https://api.mistral.ai/v1",
4727
+ apiKey,
4728
+ }));
4729
+ const getMistral = () => {
4680
4730
  const apiKey = engine$1.contextService.context.apiKey;
4681
4731
  if (Array.isArray(apiKey)) {
4682
- getMistral.clear();
4683
4732
  throw new Error("Mistral provider does not support token rotation");
4684
4733
  }
4685
- return new OpenAI({
4686
- baseURL: "https://api.mistral.ai/v1",
4687
- apiKey: apiKey,
4688
- });
4689
- });
4734
+ return GET_CLIENT_FN$4(apiKey);
4735
+ };
4690
4736
 
4691
4737
  /**
4692
4738
  * Maximum number of retry attempts for outline completion.
@@ -4769,7 +4815,7 @@ class MistralProvider {
4769
4815
  tools: formattedTools?.length ? formattedTools : undefined,
4770
4816
  });
4771
4817
  const result = {
4772
- content: content,
4818
+ content: content ?? "",
4773
4819
  mode,
4774
4820
  agentName,
4775
4821
  role,
@@ -4952,7 +4998,12 @@ class MistralProvider {
4952
4998
  attempt++;
4953
4999
  continue;
4954
5000
  }
4955
- set(validation.data, "_context", this.contextService.context);
5001
+ {
5002
+ // apiKey must never reach the outline result: it gets dumped to
5003
+ // markdown files and forwarded to downstream consumers.
5004
+ const { inference, model } = this.contextService.context;
5005
+ set(validation.data, "_context", { inference, model });
5006
+ }
4956
5007
  const result = {
4957
5008
  role: "assistant",
4958
5009
  content: JSON.stringify(validation.data),
@@ -4998,17 +5049,17 @@ class MistralProvider {
4998
5049
  * });
4999
5050
  * ```
5000
5051
  */
5001
- const getPerplexity = singleshot(() => {
5052
+ const GET_CLIENT_FN$3 = memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
5053
+ baseURL: "https://api.perplexity.ai",
5054
+ apiKey,
5055
+ }));
5056
+ const getPerplexity = () => {
5002
5057
  const apiKey = engine$1.contextService.context.apiKey;
5003
5058
  if (Array.isArray(apiKey)) {
5004
- getPerplexity.clear();
5005
5059
  throw new Error("Perplexity provider does not support token rotation");
5006
5060
  }
5007
- return new OpenAI({
5008
- baseURL: "https://api.perplexity.ai",
5009
- apiKey: apiKey,
5010
- });
5011
- });
5061
+ return GET_CLIENT_FN$3(apiKey);
5062
+ };
5012
5063
 
5013
5064
  /**
5014
5065
  * Provider for Perplexity AI models via OpenAI-compatible API.
@@ -5124,7 +5175,7 @@ class PerplexityProvider {
5124
5175
  });
5125
5176
  const { choices: [{ message: { content, role, tool_calls }, },], } = result;
5126
5177
  const finalResult = {
5127
- content: content,
5178
+ content: content ?? "",
5128
5179
  mode,
5129
5180
  agentName,
5130
5181
  role,
@@ -5251,6 +5302,10 @@ class PerplexityProvider {
5251
5302
  }
5252
5303
  }
5253
5304
 
5305
+ const GET_CLIENT_FN$2 = memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
5306
+ baseURL: "https://api.cohere.ai/compatibility/v1",
5307
+ apiKey,
5308
+ }));
5254
5309
  /**
5255
5310
  * Creates and caches an OpenAI-compatible client for Cohere API.
5256
5311
  *
@@ -5278,17 +5333,13 @@ class PerplexityProvider {
5278
5333
  * });
5279
5334
  * ```
5280
5335
  */
5281
- const getCohere = singleshot(() => {
5336
+ const getCohere = () => {
5282
5337
  const apiKey = engine$1.contextService.context.apiKey;
5283
5338
  if (Array.isArray(apiKey)) {
5284
- getCohere.clear();
5285
5339
  throw new Error("Cohere provider does not support token rotation");
5286
5340
  }
5287
- return new OpenAI({
5288
- baseURL: "https://api.cohere.ai/compatibility/v1",
5289
- apiKey: apiKey,
5290
- });
5291
- });
5341
+ return GET_CLIENT_FN$2(apiKey);
5342
+ };
5292
5343
 
5293
5344
  /**
5294
5345
  * Provider for Cohere AI models via OpenAI-compatible API.
@@ -5398,7 +5449,7 @@ class CohereProvider {
5398
5449
  });
5399
5450
  const { choices: [{ message: { content, role, tool_calls }, },], } = result;
5400
5451
  const finalResult = {
5401
- content: content,
5452
+ content: content ?? "",
5402
5453
  mode,
5403
5454
  agentName,
5404
5455
  role,
@@ -5678,7 +5729,7 @@ class AlibabaProvider {
5678
5729
  });
5679
5730
  const { choices: [{ message: { content, role, tool_calls }, },], } = responseData;
5680
5731
  const result = {
5681
- content: content,
5732
+ content: content ?? "",
5682
5733
  mode,
5683
5734
  agentName,
5684
5735
  role,
@@ -5759,7 +5810,7 @@ class AlibabaProvider {
5759
5810
  });
5760
5811
  }
5761
5812
  const result = {
5762
- content: content,
5813
+ content: content ?? "",
5763
5814
  mode,
5764
5815
  agentName,
5765
5816
  role,
@@ -5887,7 +5938,12 @@ class AlibabaProvider {
5887
5938
  attempt++;
5888
5939
  continue;
5889
5940
  }
5890
- set(validation.data, "_context", this.contextService.context);
5941
+ {
5942
+ // apiKey must never reach the outline result: it gets dumped to
5943
+ // markdown files and forwarded to downstream consumers.
5944
+ const { inference, model } = this.contextService.context;
5945
+ set(validation.data, "_context", { inference, model });
5946
+ }
5891
5947
  const result = {
5892
5948
  role: "assistant",
5893
5949
  content: JSON.stringify(validation.data),
@@ -5906,6 +5962,10 @@ class AlibabaProvider {
5906
5962
  }
5907
5963
  }
5908
5964
 
5965
+ const GET_CLIENT_FN$1 = memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
5966
+ apiKey,
5967
+ baseURL: "https://api.z.ai/api/paas/v4/",
5968
+ }));
5909
5969
  /**
5910
5970
  * Creates and caches an OpenAI-compatible client for Z.ai GLM-4 API.
5911
5971
  *
@@ -5948,17 +6008,13 @@ class AlibabaProvider {
5948
6008
  * });
5949
6009
  * ```
5950
6010
  */
5951
- const getZAi = singleshot(() => {
6011
+ const getZAi = () => {
5952
6012
  const apiKey = engine$1.contextService.context.apiKey;
5953
6013
  if (Array.isArray(apiKey)) {
5954
- getZAi.clear();
5955
6014
  throw new Error("Z.ai provider does not support token rotation");
5956
6015
  }
5957
- return new OpenAI({
5958
- apiKey,
5959
- baseURL: "https://api.z.ai/api/paas/v4/"
5960
- });
5961
- });
6016
+ return GET_CLIENT_FN$1(apiKey);
6017
+ };
5962
6018
 
5963
6019
  /**
5964
6020
  * GLM-4 provider implementation for Z.ai API integration.
@@ -6090,7 +6146,7 @@ class GLM4Provider {
6090
6146
  tools: tools,
6091
6147
  });
6092
6148
  const result = {
6093
- content: content,
6149
+ content: content ?? "",
6094
6150
  mode,
6095
6151
  agentName,
6096
6152
  role,
@@ -6324,16 +6380,16 @@ class GLM4Provider {
6324
6380
  * });
6325
6381
  * ```
6326
6382
  */
6327
- const getGroq = singleshot(() => {
6383
+ const GET_CLIENT_FN = memoize(([apiKey]) => `${apiKey}`, (apiKey) => new Groq({
6384
+ apiKey,
6385
+ }));
6386
+ const getGroq = () => {
6328
6387
  const apiKey = engine$1.contextService.context.apiKey;
6329
6388
  if (Array.isArray(apiKey)) {
6330
- getGroq.clear();
6331
6389
  throw new Error("Groq provider does not support token rotation");
6332
6390
  }
6333
- return new Groq({
6334
- apiKey: apiKey,
6335
- });
6336
- });
6391
+ return GET_CLIENT_FN(apiKey);
6392
+ };
6337
6393
 
6338
6394
  /**
6339
6395
  * Maximum number of retry attempts for outline completion.
@@ -6416,7 +6472,7 @@ class GroqProvider {
6416
6472
  tools: formattedTools?.length ? formattedTools : undefined,
6417
6473
  });
6418
6474
  const result = {
6419
- content: content,
6475
+ content: content ?? "",
6420
6476
  mode,
6421
6477
  agentName,
6422
6478
  role,
@@ -6594,7 +6650,12 @@ class GroqProvider {
6594
6650
  attempt++;
6595
6651
  continue;
6596
6652
  }
6597
- set(validation.data, "_context", this.contextService.context);
6653
+ {
6654
+ // apiKey must never reach the outline result: it gets dumped to
6655
+ // markdown files and forwarded to downstream consumers.
6656
+ const { inference, model } = this.contextService.context;
6657
+ set(validation.data, "_context", { inference, model });
6658
+ }
6598
6659
  const result = {
6599
6660
  role: "assistant",
6600
6661
  content: JSON.stringify(validation.data),
@@ -6814,7 +6875,7 @@ addCompletion({
6814
6875
  getCompletion: async (params) => {
6815
6876
  const result = await LOCAL_RUNNER_FN$1(params);
6816
6877
  if (typeof result === "symbol") {
6817
- throw new Error(`${CompletionName.RunnerCompletion} inference timeout`);
6878
+ throw new Error(`${CompletionName.RunnerStreamCompletion} inference timeout`);
6818
6879
  }
6819
6880
  return result;
6820
6881
  },
@@ -6882,7 +6943,7 @@ addCompletion({
6882
6943
  * ```
6883
6944
  */
6884
6945
  const ollama = (fn, model, apiKey) => {
6885
- const wrappedFn = async (args) => {
6946
+ const wrappedFn = async (...args) => {
6886
6947
  return await ContextService.runInContext(async () => {
6887
6948
  return await fn(...args);
6888
6949
  }, {
@@ -6914,7 +6975,7 @@ const ollama = (fn, model, apiKey) => {
6914
6975
  * ```
6915
6976
  */
6916
6977
  const grok = (fn, model, apiKey) => {
6917
- const wrappedFn = async (args) => {
6978
+ const wrappedFn = async (...args) => {
6918
6979
  return await ContextService.runInContext(async () => {
6919
6980
  return await fn(...args);
6920
6981
  }, {
@@ -6946,7 +7007,7 @@ const grok = (fn, model, apiKey) => {
6946
7007
  * ```
6947
7008
  */
6948
7009
  const hf = (fn, model, apiKey) => {
6949
- const wrappedFn = async (args) => {
7010
+ const wrappedFn = async (...args) => {
6950
7011
  return await ContextService.runInContext(async () => {
6951
7012
  return await fn(...args);
6952
7013
  }, {
@@ -6978,7 +7039,7 @@ const hf = (fn, model, apiKey) => {
6978
7039
  * ```
6979
7040
  */
6980
7041
  const claude = (fn, model, apiKey) => {
6981
- const wrappedFn = async (args) => {
7042
+ const wrappedFn = async (...args) => {
6982
7043
  return await ContextService.runInContext(async () => {
6983
7044
  return await fn(...args);
6984
7045
  }, {
@@ -7010,7 +7071,7 @@ const claude = (fn, model, apiKey) => {
7010
7071
  * ```
7011
7072
  */
7012
7073
  const gpt5 = (fn, model, apiKey) => {
7013
- const wrappedFn = async (args) => {
7074
+ const wrappedFn = async (...args) => {
7014
7075
  return await ContextService.runInContext(async () => {
7015
7076
  return await fn(...args);
7016
7077
  }, {
@@ -7042,7 +7103,7 @@ const gpt5 = (fn, model, apiKey) => {
7042
7103
  * ```
7043
7104
  */
7044
7105
  const deepseek = (fn, model, apiKey) => {
7045
- const wrappedFn = async (args) => {
7106
+ const wrappedFn = async (...args) => {
7046
7107
  return await ContextService.runInContext(async () => {
7047
7108
  return await fn(...args);
7048
7109
  }, {
@@ -7074,7 +7135,7 @@ const deepseek = (fn, model, apiKey) => {
7074
7135
  * ```
7075
7136
  */
7076
7137
  const mistral = (fn, model, apiKey) => {
7077
- const wrappedFn = async (args) => {
7138
+ const wrappedFn = async (...args) => {
7078
7139
  return await ContextService.runInContext(async () => {
7079
7140
  return await fn(...args);
7080
7141
  }, {
@@ -7106,7 +7167,7 @@ const mistral = (fn, model, apiKey) => {
7106
7167
  * ```
7107
7168
  */
7108
7169
  const perplexity = (fn, model, apiKey) => {
7109
- const wrappedFn = async (args) => {
7170
+ const wrappedFn = async (...args) => {
7110
7171
  return await ContextService.runInContext(async () => {
7111
7172
  return await fn(...args);
7112
7173
  }, {
@@ -7138,7 +7199,7 @@ const perplexity = (fn, model, apiKey) => {
7138
7199
  * ```
7139
7200
  */
7140
7201
  const cohere = (fn, model, apiKey) => {
7141
- const wrappedFn = async (args) => {
7202
+ const wrappedFn = async (...args) => {
7142
7203
  return await ContextService.runInContext(async () => {
7143
7204
  return await fn(...args);
7144
7205
  }, {
@@ -7170,7 +7231,7 @@ const cohere = (fn, model, apiKey) => {
7170
7231
  * ```
7171
7232
  */
7172
7233
  const alibaba = (fn, model, apiKey) => {
7173
- const wrappedFn = async (args) => {
7234
+ const wrappedFn = async (...args) => {
7174
7235
  return await ContextService.runInContext(async () => {
7175
7236
  return await fn(...args);
7176
7237
  }, {
@@ -7181,26 +7242,6 @@ const alibaba = (fn, model, apiKey) => {
7181
7242
  };
7182
7243
  return wrappedFn;
7183
7244
  };
7184
- /**
7185
- * Wrap async function with Zhipu AI GLM-4 inference context.
7186
- *
7187
- * Creates a higher-order function that executes the provided async function
7188
- * within a Zhipu AI GLM-4 inference context via OpenAI-compatible Z.ai API.
7189
- *
7190
- * @template T - Async function type
7191
- * @param fn - Async function to wrap
7192
- * @param model - GLM-4 model name (e.g., "glm-4-plus", "glm-4-air")
7193
- * @param apiKey - Single API key or array of keys
7194
- * @returns Wrapped function with same signature as input
7195
- *
7196
- * @example
7197
- * ```typescript
7198
- * import { glm4 } from '@backtest-kit/ollama';
7199
- *
7200
- * const wrappedFn = glm4(myAsyncFn, 'glm-4-plus', process.env.ZAI_API_KEY);
7201
- * const result = await wrappedFn(args);
7202
- * ```
7203
- */
7204
7245
  /**
7205
7246
  * Wrap async function with Groq inference context.
7206
7247
  *
@@ -7222,7 +7263,7 @@ const alibaba = (fn, model, apiKey) => {
7222
7263
  * ```
7223
7264
  */
7224
7265
  const groq = (fn, model, apiKey) => {
7225
- const wrappedFn = async (args) => {
7266
+ const wrappedFn = async (...args) => {
7226
7267
  return await ContextService.runInContext(async () => {
7227
7268
  return await fn(...args);
7228
7269
  }, {
@@ -7233,8 +7274,28 @@ const groq = (fn, model, apiKey) => {
7233
7274
  };
7234
7275
  return wrappedFn;
7235
7276
  };
7277
+ /**
7278
+ * Wrap async function with Zhipu AI GLM-4 inference context.
7279
+ *
7280
+ * Creates a higher-order function that executes the provided async function
7281
+ * within a Zhipu AI GLM-4 inference context via OpenAI-compatible Z.ai API.
7282
+ *
7283
+ * @template T - Async function type
7284
+ * @param fn - Async function to wrap
7285
+ * @param model - GLM-4 model name (e.g., "glm-4-plus", "glm-4-air")
7286
+ * @param apiKey - Single API key or array of keys
7287
+ * @returns Wrapped function with same signature as input
7288
+ *
7289
+ * @example
7290
+ * ```typescript
7291
+ * import { glm4 } from '@backtest-kit/ollama';
7292
+ *
7293
+ * const wrappedFn = glm4(myAsyncFn, 'glm-4-plus', process.env.ZAI_API_KEY);
7294
+ * const result = await wrappedFn(args);
7295
+ * ```
7296
+ */
7236
7297
  const glm4 = (fn, model, apiKey) => {
7237
- const wrappedFn = async (args) => {
7298
+ const wrappedFn = async (...args) => {
7238
7299
  return await ContextService.runInContext(async () => {
7239
7300
  return await fn(...args);
7240
7301
  }, {
@@ -7287,7 +7348,8 @@ const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
7287
7348
  *
7288
7349
  * @example
7289
7350
  * ```typescript
7290
- * import { dumpSignal, getCandles } from "backtest-kit";
7351
+ * import { getCandles } from "backtest-kit";
7352
+ * import { dumpSignal } from "@backtest-kit/ollama";
7291
7353
  * import { v4 as uuid } from "uuid";
7292
7354
  *
7293
7355
  * addStrategy({
@@ -7434,7 +7496,9 @@ async function validate(args = {}) {
7434
7496
  return await validateInternal(args);
7435
7497
  }
7436
7498
 
7437
- const MODULE_TYPE_SYMBOL = Symbol("module-type");
7499
+ // Symbol.for keeps the brand check working when CJS and ESM copies of the
7500
+ // package end up in the same process (dual-package hazard).
7501
+ const MODULE_TYPE_SYMBOL = Symbol.for("backtest-kit.ollama.module-type");
7438
7502
  class Module {
7439
7503
  constructor(path, baseDir) {
7440
7504
  this.path = path;
@@ -7495,15 +7559,15 @@ async function commitPrompt(source, history) {
7495
7559
  const systemPrompts = await engine$1.resolvePromptService.getSystemPrompt(prompt, symbol, strategyName, exchangeName, frameName, isBacktest);
7496
7560
  const userPrompt = await engine$1.resolvePromptService.getUserPrompt(prompt, symbol, strategyName, exchangeName, frameName, isBacktest);
7497
7561
  if (systemPrompts.length > 0) {
7498
- for (const content of systemPrompts) {
7499
- if (!content.trim()) {
7500
- continue;
7501
- }
7502
- history.unshift({
7503
- role: "system",
7504
- content,
7505
- });
7506
- }
7562
+ // Single unshift with spread: per-item unshift in a loop would REVERSE
7563
+ // the system prompt order at the head of the history.
7564
+ const systemMessages = systemPrompts
7565
+ .filter((content) => content.trim())
7566
+ .map((content) => ({
7567
+ role: "system",
7568
+ content,
7569
+ }));
7570
+ history.unshift(...systemMessages);
7507
7571
  }
7508
7572
  if (userPrompt && userPrompt.trim()) {
7509
7573
  history.push({
@@ -7683,7 +7747,7 @@ const LIST_OPTIMIZERS_METHOD_NAME = "list.listOptimizerSchema";
7683
7747
  *
7684
7748
  * @example
7685
7749
  * ```typescript
7686
- * import { listOptimizers, addOptimizer } from "backtest-kit";
7750
+ * import { listOptimizers, addOptimizer } from "@backtest-kit/ollama";
7687
7751
  *
7688
7752
  * addOptimizer({
7689
7753
  * optimizerName: "llm-strategy-generator",