@backtest-kit/ollama 14.1.0 → 15.0.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.cjs CHANGED
@@ -297,7 +297,9 @@ const TYPES = {
297
297
  ...publicServices$1,
298
298
  };
299
299
 
300
- const PROMPT_TYPE_SYMBOL = Symbol("prompt-type");
300
+ // Symbol.for keeps the brand check working when CJS and ESM copies of the
301
+ // package end up in the same process (dual-package hazard).
302
+ const PROMPT_TYPE_SYMBOL = Symbol.for("backtest-kit.ollama.prompt-type");
301
303
  class Prompt {
302
304
  constructor(source) {
303
305
  this.source = source;
@@ -317,7 +319,7 @@ Prompt.isPrompt = (value) => {
317
319
  };
318
320
 
319
321
  const require$1 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
320
- const REQUIRE_MODULE_FN = functoolsKit.memoize(([module]) => module, (module) => {
322
+ const REQUIRE_MODULE_FN = functoolsKit.memoize(([module]) => path.join(module.baseDir, module.path), (module) => {
321
323
  const modulePath = require$1.resolve(path.join(module.baseDir, module.path));
322
324
  return require$1(modulePath);
323
325
  });
@@ -337,7 +339,8 @@ class PromptCacheService {
337
339
  module,
338
340
  });
339
341
  if (module) {
340
- REQUIRE_MODULE_FN.clear(module);
342
+ REQUIRE_MODULE_FN.clear(path.join(module.baseDir, module.path));
343
+ return;
341
344
  }
342
345
  REQUIRE_MODULE_FN.clear();
343
346
  };
@@ -898,6 +901,13 @@ const toPlainString = async (content) => {
898
901
  typographer: true,
899
902
  });
900
903
  let telegramHtml = md.render(markdown);
904
+ // sanitize-html transformTags cannot insert text content, so list markers
905
+ // are injected into the HTML as text before sanitizing.
906
+ telegramHtml = telegramHtml.replace(/<ol[^>]*>[\s\S]*?<\/ol>/g, (block) => {
907
+ let counter = 0;
908
+ return block.replace(/<li>/g, () => `<li>${++counter}. `);
909
+ });
910
+ telegramHtml = telegramHtml.replace(/<li>(?!\d+\. )/g, "<li>• ");
901
911
  telegramHtml = sanitizeHtml(telegramHtml, {
902
912
  allowedTags: [
903
913
  "b",
@@ -926,7 +936,7 @@ const toPlainString = async (content) => {
926
936
  em: "",
927
937
  p: () => "",
928
938
  ul: () => "",
929
- li: () => "",
939
+ li: () => "",
930
940
  ol: () => "",
931
941
  hr: () => "\n",
932
942
  br: () => "\n",
@@ -1195,6 +1205,10 @@ class OptimizerTemplateService {
1195
1205
  ``,
1196
1206
  ` await dumpJson(resultId, messages, result);`,
1197
1207
  ``,
1208
+ ` if (result.position === "wait") {`,
1209
+ ` return null;`,
1210
+ ` }`,
1211
+ ``,
1198
1212
  ` result.id = resultId;`,
1199
1213
  ``,
1200
1214
  ` return result;`,
@@ -1881,8 +1895,9 @@ const GET_STRATEGY_DATA_FN = async (symbol, self) => {
1881
1895
  const strategyList = [];
1882
1896
  const totalSources = self.params.rangeTrain.length * self.params.source.length;
1883
1897
  let processedSources = 0;
1884
- for (const { startDate, endDate } of self.params.rangeTrain) {
1898
+ for (const { note, startDate, endDate } of self.params.rangeTrain) {
1885
1899
  const messageList = [];
1900
+ const sourceNames = [];
1886
1901
  for (const source of self.params.source) {
1887
1902
  // Emit progress event at the start of processing each source
1888
1903
  await self.onProgress({
@@ -1910,6 +1925,7 @@ const GET_STRATEGY_DATA_FN = async (symbol, self) => {
1910
1925
  role: "assistant",
1911
1926
  content: assistantContent,
1912
1927
  });
1928
+ sourceNames.push(DEFAULT_SOURCE_NAME);
1913
1929
  processedSources++;
1914
1930
  }
1915
1931
  else {
@@ -1931,18 +1947,19 @@ const GET_STRATEGY_DATA_FN = async (symbol, self) => {
1931
1947
  role: "assistant",
1932
1948
  content: assistantContent,
1933
1949
  });
1950
+ sourceNames.push(name || DEFAULT_SOURCE_NAME);
1934
1951
  processedSources++;
1935
1952
  }
1936
- const name = "name" in source
1937
- ? source.name || DEFAULT_SOURCE_NAME
1938
- : DEFAULT_SOURCE_NAME;
1939
- strategyList.push({
1940
- symbol,
1941
- name,
1942
- messages: messageList,
1943
- strategy: await self.params.getPrompt(symbol, messageList),
1944
- });
1945
1953
  }
1954
+ // One strategy per training range: getPrompt receives the complete
1955
+ // conversation history with all sources (see IOptimizerSchema.getPrompt).
1956
+ // Messages are snapshotted so later ranges cannot mutate this strategy.
1957
+ strategyList.push({
1958
+ symbol,
1959
+ name: note || sourceNames.join("+") || DEFAULT_SOURCE_NAME,
1960
+ messages: [...messageList],
1961
+ strategy: await self.params.getPrompt(symbol, messageList),
1962
+ });
1946
1963
  }
1947
1964
  // Emit final progress event (100%)
1948
1965
  await self.onProgress({
@@ -2048,7 +2065,7 @@ const GET_STRATEGY_CODE_FN = async (symbol, self) => {
2048
2065
  const GET_STRATEGY_DUMP_FN = async (symbol, path$1, self) => {
2049
2066
  const report = await self.getCode(symbol);
2050
2067
  try {
2051
- const dir = path.join(process.cwd(), path$1);
2068
+ const dir = path.isAbsolute(path$1) ? path$1 : path.join(process.cwd(), path$1);
2052
2069
  await fs.mkdir(dir, { recursive: true });
2053
2070
  const filename = `${self.params.optimizerName}_${symbol}.mjs`;
2054
2071
  const filepath = path.join(dir, filename);
@@ -2438,17 +2455,17 @@ var InferenceName$1 = InferenceName;
2438
2455
  * });
2439
2456
  * ```
2440
2457
  */
2441
- const getGrok = functoolsKit.singleshot(() => {
2458
+ const GET_CLIENT_FN$9 = functoolsKit.memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
2459
+ baseURL: "https://api.x.ai/v1",
2460
+ apiKey,
2461
+ }));
2462
+ const getGrok = () => {
2442
2463
  const apiKey = engine$1.contextService.context.apiKey;
2443
2464
  if (Array.isArray(apiKey)) {
2444
- getGrok.clear();
2445
2465
  throw new Error("Grok provider does not support token rotation");
2446
2466
  }
2447
- return new OpenAI({
2448
- baseURL: "https://api.x.ai/v1",
2449
- apiKey: apiKey,
2450
- });
2451
- });
2467
+ return GET_CLIENT_FN$9(apiKey);
2468
+ };
2452
2469
 
2453
2470
  /**
2454
2471
  * Global configuration parameters for the Ollama package.
@@ -2584,7 +2601,7 @@ class GrokProvider {
2584
2601
  },
2585
2602
  });
2586
2603
  const result = {
2587
- content: content,
2604
+ content: content ?? "",
2588
2605
  mode,
2589
2606
  agentName,
2590
2607
  role,
@@ -2744,8 +2761,8 @@ class GrokProvider {
2744
2761
  Authorization: `Bearer ${this.contextService.context.apiKey}`,
2745
2762
  },
2746
2763
  body: JSON.stringify({
2764
+ model: this.contextService.context.model,
2747
2765
  messages,
2748
- context: this.contextService.context,
2749
2766
  max_tokens: 5000,
2750
2767
  response_format: format,
2751
2768
  }),
@@ -3131,7 +3148,12 @@ class HfProvider {
3131
3148
  continue;
3132
3149
  }
3133
3150
  lodashEs.set(validation.data, "_thinking", reasoning_content);
3134
- lodashEs.set(validation.data, "_context", this.contextService.context);
3151
+ {
3152
+ // apiKey must never reach the outline result: it gets dumped to
3153
+ // markdown files and forwarded to downstream consumers.
3154
+ const { inference, model } = this.contextService.context;
3155
+ lodashEs.set(validation.data, "_context", { inference, model });
3156
+ }
3135
3157
  const result = {
3136
3158
  role: "assistant",
3137
3159
  content: JSON.stringify(validation.data),
@@ -3166,10 +3188,12 @@ class HfProvider {
3166
3188
  * @throws Error if no API keys are provided in context
3167
3189
  */
3168
3190
  class OllamaWrapper {
3169
- constructor(_config) {
3191
+ constructor(_config, apiKeys) {
3170
3192
  this._config = _config;
3171
- /** Round-robin chat function factory */
3172
- this._chatFn = agentSwarmKit.RoundRobin.create(engine$1.contextService.context.apiKey, (token) => {
3193
+ if (!apiKeys?.length) {
3194
+ throw new Error("OllamaRotate required apiKey[] to process token rotation");
3195
+ }
3196
+ this._chatFn = agentSwarmKit.RoundRobin.create(apiKeys, (token) => {
3173
3197
  const ollama = new ollama$1.Ollama({
3174
3198
  ...this._config,
3175
3199
  headers: {
@@ -3185,9 +3209,6 @@ class OllamaWrapper {
3185
3209
  }
3186
3210
  };
3187
3211
  });
3188
- if (!engine$1.contextService.context.apiKey) {
3189
- throw new Error("OllamaRotate required apiKey[] to process token rotation");
3190
- }
3191
3212
  }
3192
3213
  /**
3193
3214
  * Executes a chat request with automatic token rotation.
@@ -3220,10 +3241,28 @@ class OllamaWrapper {
3220
3241
  * // Next request will use a different API key
3221
3242
  * ```
3222
3243
  */
3223
- const getOllamaRotate = functoolsKit.singleshot(() => new OllamaWrapper({
3244
+ const GET_WRAPPER_FN = functoolsKit.memoize(([apiKeys]) => apiKeys.join(","), (apiKeys) => new OllamaWrapper({
3224
3245
  host: "https://ollama.com",
3225
- }));
3246
+ }, apiKeys));
3247
+ const getOllamaRotate = () => {
3248
+ const apiKey = engine$1.contextService.context.apiKey;
3249
+ if (!Array.isArray(apiKey)) {
3250
+ throw new Error("OllamaRotate required apiKey[] to process token rotation");
3251
+ }
3252
+ return GET_WRAPPER_FN(apiKey);
3253
+ };
3226
3254
 
3255
+ const GET_CLIENT_FN$8 = functoolsKit.memoize(([apiKey]) => `${apiKey ?? ""}`, (apiKey) => {
3256
+ if (!apiKey) {
3257
+ return new ollama$1.Ollama();
3258
+ }
3259
+ return new ollama$1.Ollama({
3260
+ host: "https://ollama.com",
3261
+ headers: {
3262
+ Authorization: `Bearer ${apiKey}`,
3263
+ },
3264
+ });
3265
+ });
3227
3266
  /**
3228
3267
  * Creates and caches an Ollama client with flexible configuration.
3229
3268
  *
@@ -3269,21 +3308,13 @@ const getOllamaRotate = functoolsKit.singleshot(() => new OllamaWrapper({
3269
3308
  * });
3270
3309
  * ```
3271
3310
  */
3272
- const getOllama = functoolsKit.singleshot(() => {
3311
+ const getOllama = () => {
3273
3312
  const apiKey = engine$1.contextService.context.apiKey;
3274
3313
  if (Array.isArray(apiKey)) {
3275
3314
  return getOllamaRotate();
3276
3315
  }
3277
- if (!apiKey) {
3278
- return new ollama$1.Ollama();
3279
- }
3280
- return new ollama$1.Ollama({
3281
- host: "https://ollama.com",
3282
- headers: {
3283
- Authorization: `Bearer ${apiKey}`,
3284
- },
3285
- });
3286
- });
3316
+ return GET_CLIENT_FN$8(apiKey);
3317
+ };
3287
3318
 
3288
3319
  /**
3289
3320
  * Maximum number of retry attempts for outline completion when model fails to use tools correctly.
@@ -3617,7 +3648,12 @@ class OllamaProvider {
3617
3648
  attempt++;
3618
3649
  continue;
3619
3650
  }
3620
- lodashEs.set(validation.data, "_context", this.contextService.context);
3651
+ {
3652
+ // apiKey must never reach the outline result: it gets dumped to
3653
+ // markdown files and forwarded to downstream consumers.
3654
+ const { inference, model } = this.contextService.context;
3655
+ lodashEs.set(validation.data, "_context", { inference, model });
3656
+ }
3621
3657
  const result = {
3622
3658
  role: "assistant",
3623
3659
  content: JSON.stringify(validation.data),
@@ -3636,6 +3672,10 @@ class OllamaProvider {
3636
3672
  }
3637
3673
  }
3638
3674
 
3675
+ const GET_CLIENT_FN$7 = functoolsKit.memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
3676
+ baseURL: "https://api.anthropic.com/v1/",
3677
+ apiKey,
3678
+ }));
3639
3679
  /**
3640
3680
  * Creates and caches an OpenAI-compatible client for Claude (Anthropic) API.
3641
3681
  *
@@ -3663,17 +3703,13 @@ class OllamaProvider {
3663
3703
  * });
3664
3704
  * ```
3665
3705
  */
3666
- const getClaude = functoolsKit.singleshot(() => {
3706
+ const getClaude = () => {
3667
3707
  const apiKey = engine$1.contextService.context.apiKey;
3668
3708
  if (Array.isArray(apiKey)) {
3669
- getClaude.clear();
3670
3709
  throw new Error("Claude provider does not support token rotation");
3671
3710
  }
3672
- return new OpenAI({
3673
- baseURL: "https://api.anthropic.com/v1/",
3674
- apiKey,
3675
- });
3676
- });
3711
+ return GET_CLIENT_FN$7(apiKey);
3712
+ };
3677
3713
 
3678
3714
  /**
3679
3715
  * Maximum number of retry attempts for outline completion when model fails to use tools correctly.
@@ -3776,7 +3812,7 @@ class ClaudeProvider {
3776
3812
  }
3777
3813
  const { choices: [{ message: { content, role, tool_calls }, },], } = await claude.chat.completions.create(requestOptions);
3778
3814
  const result = {
3779
- content: content,
3815
+ content: content ?? "",
3780
3816
  mode,
3781
3817
  agentName,
3782
3818
  role,
@@ -3857,7 +3893,7 @@ class ClaudeProvider {
3857
3893
  };
3858
3894
  // Debug logging
3859
3895
  if (GLOBAL_CONFIG.CC_ENABLE_DEBUG) {
3860
- await fs.appendFile("./debug_gpt5_provider_stream.txt", JSON.stringify({ params, answer: result }, null, 2) + "\n\n");
3896
+ await fs.appendFile("./debug_claude_provider_stream.txt", JSON.stringify({ params, answer: result }, null, 2) + "\n\n");
3861
3897
  }
3862
3898
  return result;
3863
3899
  }
@@ -3960,7 +3996,12 @@ class ClaudeProvider {
3960
3996
  attempt++;
3961
3997
  continue;
3962
3998
  }
3963
- lodashEs.set(validation.data, "_context", this.contextService.context);
3999
+ {
4000
+ // apiKey must never reach the outline result: it gets dumped to
4001
+ // markdown files and forwarded to downstream consumers.
4002
+ const { inference, model } = this.contextService.context;
4003
+ lodashEs.set(validation.data, "_context", { inference, model });
4004
+ }
3964
4005
  const result = {
3965
4006
  role: "assistant",
3966
4007
  content: JSON.stringify(validation.data),
@@ -3979,6 +4020,9 @@ class ClaudeProvider {
3979
4020
  }
3980
4021
  }
3981
4022
 
4023
+ const GET_CLIENT_FN$6 = functoolsKit.memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
4024
+ apiKey,
4025
+ }));
3982
4026
  /**
3983
4027
  * Creates and caches an OpenAI client for OpenAI API.
3984
4028
  *
@@ -4006,16 +4050,13 @@ class ClaudeProvider {
4006
4050
  * });
4007
4051
  * ```
4008
4052
  */
4009
- const getOpenAi = functoolsKit.singleshot(() => {
4053
+ const getOpenAi = () => {
4010
4054
  const apiKey = engine$1.contextService.context.apiKey;
4011
4055
  if (Array.isArray(apiKey)) {
4012
- getOpenAi.clear();
4013
4056
  throw new Error("OpenAI provider does not support token rotation");
4014
4057
  }
4015
- return new OpenAI({
4016
- apiKey: apiKey,
4017
- });
4018
- });
4058
+ return GET_CLIENT_FN$6(apiKey);
4059
+ };
4019
4060
 
4020
4061
  /**
4021
4062
  * Provider for OpenAI GPT models (GPT-4, GPT-4 Turbo, GPT-3.5, etc.).
@@ -4132,7 +4173,7 @@ class GPT5Provider {
4132
4173
  tools: tools,
4133
4174
  });
4134
4175
  const result = {
4135
- content: content,
4176
+ content: content ?? "",
4136
4177
  mode,
4137
4178
  agentName,
4138
4179
  role,
@@ -4357,17 +4398,17 @@ class GPT5Provider {
4357
4398
  * });
4358
4399
  * ```
4359
4400
  */
4360
- const getDeepseek = functoolsKit.singleshot(() => {
4401
+ const GET_CLIENT_FN$5 = functoolsKit.memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
4402
+ baseURL: "https://api.deepseek.com",
4403
+ apiKey,
4404
+ }));
4405
+ const getDeepseek = () => {
4361
4406
  const apiKey = engine$1.contextService.context.apiKey;
4362
4407
  if (Array.isArray(apiKey)) {
4363
- getDeepseek.clear();
4364
4408
  throw new Error("Deepseek provider does not support token rotation");
4365
4409
  }
4366
- return new OpenAI({
4367
- baseURL: "https://api.deepseek.com",
4368
- apiKey: apiKey,
4369
- });
4370
- });
4410
+ return GET_CLIENT_FN$5(apiKey);
4411
+ };
4371
4412
 
4372
4413
  /**
4373
4414
  * Maximum number of retry attempts for outline completion.
@@ -4450,7 +4491,7 @@ class DeepseekProvider {
4450
4491
  tools: formattedTools?.length ? formattedTools : undefined,
4451
4492
  });
4452
4493
  const result = {
4453
- content: content,
4494
+ content: content ?? "",
4454
4495
  mode,
4455
4496
  agentName,
4456
4497
  role,
@@ -4633,7 +4674,12 @@ class DeepseekProvider {
4633
4674
  attempt++;
4634
4675
  continue;
4635
4676
  }
4636
- lodashEs.set(validation.data, "_context", this.contextService.context);
4677
+ {
4678
+ // apiKey must never reach the outline result: it gets dumped to
4679
+ // markdown files and forwarded to downstream consumers.
4680
+ const { inference, model } = this.contextService.context;
4681
+ lodashEs.set(validation.data, "_context", { inference, model });
4682
+ }
4637
4683
  const result = {
4638
4684
  role: "assistant",
4639
4685
  content: JSON.stringify(validation.data),
@@ -4679,17 +4725,17 @@ class DeepseekProvider {
4679
4725
  * });
4680
4726
  * ```
4681
4727
  */
4682
- const getMistral = functoolsKit.singleshot(() => {
4728
+ const GET_CLIENT_FN$4 = functoolsKit.memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
4729
+ baseURL: "https://api.mistral.ai/v1",
4730
+ apiKey,
4731
+ }));
4732
+ const getMistral = () => {
4683
4733
  const apiKey = engine$1.contextService.context.apiKey;
4684
4734
  if (Array.isArray(apiKey)) {
4685
- getMistral.clear();
4686
4735
  throw new Error("Mistral provider does not support token rotation");
4687
4736
  }
4688
- return new OpenAI({
4689
- baseURL: "https://api.mistral.ai/v1",
4690
- apiKey: apiKey,
4691
- });
4692
- });
4737
+ return GET_CLIENT_FN$4(apiKey);
4738
+ };
4693
4739
 
4694
4740
  /**
4695
4741
  * Maximum number of retry attempts for outline completion.
@@ -4772,7 +4818,7 @@ class MistralProvider {
4772
4818
  tools: formattedTools?.length ? formattedTools : undefined,
4773
4819
  });
4774
4820
  const result = {
4775
- content: content,
4821
+ content: content ?? "",
4776
4822
  mode,
4777
4823
  agentName,
4778
4824
  role,
@@ -4955,7 +5001,12 @@ class MistralProvider {
4955
5001
  attempt++;
4956
5002
  continue;
4957
5003
  }
4958
- lodashEs.set(validation.data, "_context", this.contextService.context);
5004
+ {
5005
+ // apiKey must never reach the outline result: it gets dumped to
5006
+ // markdown files and forwarded to downstream consumers.
5007
+ const { inference, model } = this.contextService.context;
5008
+ lodashEs.set(validation.data, "_context", { inference, model });
5009
+ }
4959
5010
  const result = {
4960
5011
  role: "assistant",
4961
5012
  content: JSON.stringify(validation.data),
@@ -5001,17 +5052,17 @@ class MistralProvider {
5001
5052
  * });
5002
5053
  * ```
5003
5054
  */
5004
- const getPerplexity = functoolsKit.singleshot(() => {
5055
+ const GET_CLIENT_FN$3 = functoolsKit.memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
5056
+ baseURL: "https://api.perplexity.ai",
5057
+ apiKey,
5058
+ }));
5059
+ const getPerplexity = () => {
5005
5060
  const apiKey = engine$1.contextService.context.apiKey;
5006
5061
  if (Array.isArray(apiKey)) {
5007
- getPerplexity.clear();
5008
5062
  throw new Error("Perplexity provider does not support token rotation");
5009
5063
  }
5010
- return new OpenAI({
5011
- baseURL: "https://api.perplexity.ai",
5012
- apiKey: apiKey,
5013
- });
5014
- });
5064
+ return GET_CLIENT_FN$3(apiKey);
5065
+ };
5015
5066
 
5016
5067
  /**
5017
5068
  * Provider for Perplexity AI models via OpenAI-compatible API.
@@ -5127,7 +5178,7 @@ class PerplexityProvider {
5127
5178
  });
5128
5179
  const { choices: [{ message: { content, role, tool_calls }, },], } = result;
5129
5180
  const finalResult = {
5130
- content: content,
5181
+ content: content ?? "",
5131
5182
  mode,
5132
5183
  agentName,
5133
5184
  role,
@@ -5254,6 +5305,10 @@ class PerplexityProvider {
5254
5305
  }
5255
5306
  }
5256
5307
 
5308
+ const GET_CLIENT_FN$2 = functoolsKit.memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
5309
+ baseURL: "https://api.cohere.ai/compatibility/v1",
5310
+ apiKey,
5311
+ }));
5257
5312
  /**
5258
5313
  * Creates and caches an OpenAI-compatible client for Cohere API.
5259
5314
  *
@@ -5281,17 +5336,13 @@ class PerplexityProvider {
5281
5336
  * });
5282
5337
  * ```
5283
5338
  */
5284
- const getCohere = functoolsKit.singleshot(() => {
5339
+ const getCohere = () => {
5285
5340
  const apiKey = engine$1.contextService.context.apiKey;
5286
5341
  if (Array.isArray(apiKey)) {
5287
- getCohere.clear();
5288
5342
  throw new Error("Cohere provider does not support token rotation");
5289
5343
  }
5290
- return new OpenAI({
5291
- baseURL: "https://api.cohere.ai/compatibility/v1",
5292
- apiKey: apiKey,
5293
- });
5294
- });
5344
+ return GET_CLIENT_FN$2(apiKey);
5345
+ };
5295
5346
 
5296
5347
  /**
5297
5348
  * Provider for Cohere AI models via OpenAI-compatible API.
@@ -5401,7 +5452,7 @@ class CohereProvider {
5401
5452
  });
5402
5453
  const { choices: [{ message: { content, role, tool_calls }, },], } = result;
5403
5454
  const finalResult = {
5404
- content: content,
5455
+ content: content ?? "",
5405
5456
  mode,
5406
5457
  agentName,
5407
5458
  role,
@@ -5681,7 +5732,7 @@ class AlibabaProvider {
5681
5732
  });
5682
5733
  const { choices: [{ message: { content, role, tool_calls }, },], } = responseData;
5683
5734
  const result = {
5684
- content: content,
5735
+ content: content ?? "",
5685
5736
  mode,
5686
5737
  agentName,
5687
5738
  role,
@@ -5762,7 +5813,7 @@ class AlibabaProvider {
5762
5813
  });
5763
5814
  }
5764
5815
  const result = {
5765
- content: content,
5816
+ content: content ?? "",
5766
5817
  mode,
5767
5818
  agentName,
5768
5819
  role,
@@ -5890,7 +5941,12 @@ class AlibabaProvider {
5890
5941
  attempt++;
5891
5942
  continue;
5892
5943
  }
5893
- lodashEs.set(validation.data, "_context", this.contextService.context);
5944
+ {
5945
+ // apiKey must never reach the outline result: it gets dumped to
5946
+ // markdown files and forwarded to downstream consumers.
5947
+ const { inference, model } = this.contextService.context;
5948
+ lodashEs.set(validation.data, "_context", { inference, model });
5949
+ }
5894
5950
  const result = {
5895
5951
  role: "assistant",
5896
5952
  content: JSON.stringify(validation.data),
@@ -5909,6 +5965,10 @@ class AlibabaProvider {
5909
5965
  }
5910
5966
  }
5911
5967
 
5968
+ const GET_CLIENT_FN$1 = functoolsKit.memoize(([apiKey]) => `${apiKey}`, (apiKey) => new OpenAI({
5969
+ apiKey,
5970
+ baseURL: "https://api.z.ai/api/paas/v4/",
5971
+ }));
5912
5972
  /**
5913
5973
  * Creates and caches an OpenAI-compatible client for Z.ai GLM-4 API.
5914
5974
  *
@@ -5951,17 +6011,13 @@ class AlibabaProvider {
5951
6011
  * });
5952
6012
  * ```
5953
6013
  */
5954
- const getZAi = functoolsKit.singleshot(() => {
6014
+ const getZAi = () => {
5955
6015
  const apiKey = engine$1.contextService.context.apiKey;
5956
6016
  if (Array.isArray(apiKey)) {
5957
- getZAi.clear();
5958
6017
  throw new Error("Z.ai provider does not support token rotation");
5959
6018
  }
5960
- return new OpenAI({
5961
- apiKey,
5962
- baseURL: "https://api.z.ai/api/paas/v4/"
5963
- });
5964
- });
6019
+ return GET_CLIENT_FN$1(apiKey);
6020
+ };
5965
6021
 
5966
6022
  /**
5967
6023
  * GLM-4 provider implementation for Z.ai API integration.
@@ -6093,7 +6149,7 @@ class GLM4Provider {
6093
6149
  tools: tools,
6094
6150
  });
6095
6151
  const result = {
6096
- content: content,
6152
+ content: content ?? "",
6097
6153
  mode,
6098
6154
  agentName,
6099
6155
  role,
@@ -6327,16 +6383,16 @@ class GLM4Provider {
6327
6383
  * });
6328
6384
  * ```
6329
6385
  */
6330
- const getGroq = functoolsKit.singleshot(() => {
6386
+ const GET_CLIENT_FN = functoolsKit.memoize(([apiKey]) => `${apiKey}`, (apiKey) => new Groq({
6387
+ apiKey,
6388
+ }));
6389
+ const getGroq = () => {
6331
6390
  const apiKey = engine$1.contextService.context.apiKey;
6332
6391
  if (Array.isArray(apiKey)) {
6333
- getGroq.clear();
6334
6392
  throw new Error("Groq provider does not support token rotation");
6335
6393
  }
6336
- return new Groq({
6337
- apiKey: apiKey,
6338
- });
6339
- });
6394
+ return GET_CLIENT_FN(apiKey);
6395
+ };
6340
6396
 
6341
6397
  /**
6342
6398
  * Maximum number of retry attempts for outline completion.
@@ -6419,7 +6475,7 @@ class GroqProvider {
6419
6475
  tools: formattedTools?.length ? formattedTools : undefined,
6420
6476
  });
6421
6477
  const result = {
6422
- content: content,
6478
+ content: content ?? "",
6423
6479
  mode,
6424
6480
  agentName,
6425
6481
  role,
@@ -6597,7 +6653,12 @@ class GroqProvider {
6597
6653
  attempt++;
6598
6654
  continue;
6599
6655
  }
6600
- lodashEs.set(validation.data, "_context", this.contextService.context);
6656
+ {
6657
+ // apiKey must never reach the outline result: it gets dumped to
6658
+ // markdown files and forwarded to downstream consumers.
6659
+ const { inference, model } = this.contextService.context;
6660
+ lodashEs.set(validation.data, "_context", { inference, model });
6661
+ }
6601
6662
  const result = {
6602
6663
  role: "assistant",
6603
6664
  content: JSON.stringify(validation.data),
@@ -6817,7 +6878,7 @@ agentSwarmKit.addCompletion({
6817
6878
  getCompletion: async (params) => {
6818
6879
  const result = await LOCAL_RUNNER_FN$1(params);
6819
6880
  if (typeof result === "symbol") {
6820
- throw new Error(`${exports.CompletionName.RunnerCompletion} inference timeout`);
6881
+ throw new Error(`${exports.CompletionName.RunnerStreamCompletion} inference timeout`);
6821
6882
  }
6822
6883
  return result;
6823
6884
  },
@@ -6885,7 +6946,7 @@ agentSwarmKit.addCompletion({
6885
6946
  * ```
6886
6947
  */
6887
6948
  const ollama = (fn, model, apiKey) => {
6888
- const wrappedFn = async (args) => {
6949
+ const wrappedFn = async (...args) => {
6889
6950
  return await ContextService.runInContext(async () => {
6890
6951
  return await fn(...args);
6891
6952
  }, {
@@ -6917,7 +6978,7 @@ const ollama = (fn, model, apiKey) => {
6917
6978
  * ```
6918
6979
  */
6919
6980
  const grok = (fn, model, apiKey) => {
6920
- const wrappedFn = async (args) => {
6981
+ const wrappedFn = async (...args) => {
6921
6982
  return await ContextService.runInContext(async () => {
6922
6983
  return await fn(...args);
6923
6984
  }, {
@@ -6949,7 +7010,7 @@ const grok = (fn, model, apiKey) => {
6949
7010
  * ```
6950
7011
  */
6951
7012
  const hf = (fn, model, apiKey) => {
6952
- const wrappedFn = async (args) => {
7013
+ const wrappedFn = async (...args) => {
6953
7014
  return await ContextService.runInContext(async () => {
6954
7015
  return await fn(...args);
6955
7016
  }, {
@@ -6981,7 +7042,7 @@ const hf = (fn, model, apiKey) => {
6981
7042
  * ```
6982
7043
  */
6983
7044
  const claude = (fn, model, apiKey) => {
6984
- const wrappedFn = async (args) => {
7045
+ const wrappedFn = async (...args) => {
6985
7046
  return await ContextService.runInContext(async () => {
6986
7047
  return await fn(...args);
6987
7048
  }, {
@@ -7013,7 +7074,7 @@ const claude = (fn, model, apiKey) => {
7013
7074
  * ```
7014
7075
  */
7015
7076
  const gpt5 = (fn, model, apiKey) => {
7016
- const wrappedFn = async (args) => {
7077
+ const wrappedFn = async (...args) => {
7017
7078
  return await ContextService.runInContext(async () => {
7018
7079
  return await fn(...args);
7019
7080
  }, {
@@ -7045,7 +7106,7 @@ const gpt5 = (fn, model, apiKey) => {
7045
7106
  * ```
7046
7107
  */
7047
7108
  const deepseek = (fn, model, apiKey) => {
7048
- const wrappedFn = async (args) => {
7109
+ const wrappedFn = async (...args) => {
7049
7110
  return await ContextService.runInContext(async () => {
7050
7111
  return await fn(...args);
7051
7112
  }, {
@@ -7077,7 +7138,7 @@ const deepseek = (fn, model, apiKey) => {
7077
7138
  * ```
7078
7139
  */
7079
7140
  const mistral = (fn, model, apiKey) => {
7080
- const wrappedFn = async (args) => {
7141
+ const wrappedFn = async (...args) => {
7081
7142
  return await ContextService.runInContext(async () => {
7082
7143
  return await fn(...args);
7083
7144
  }, {
@@ -7109,7 +7170,7 @@ const mistral = (fn, model, apiKey) => {
7109
7170
  * ```
7110
7171
  */
7111
7172
  const perplexity = (fn, model, apiKey) => {
7112
- const wrappedFn = async (args) => {
7173
+ const wrappedFn = async (...args) => {
7113
7174
  return await ContextService.runInContext(async () => {
7114
7175
  return await fn(...args);
7115
7176
  }, {
@@ -7141,7 +7202,7 @@ const perplexity = (fn, model, apiKey) => {
7141
7202
  * ```
7142
7203
  */
7143
7204
  const cohere = (fn, model, apiKey) => {
7144
- const wrappedFn = async (args) => {
7205
+ const wrappedFn = async (...args) => {
7145
7206
  return await ContextService.runInContext(async () => {
7146
7207
  return await fn(...args);
7147
7208
  }, {
@@ -7173,7 +7234,7 @@ const cohere = (fn, model, apiKey) => {
7173
7234
  * ```
7174
7235
  */
7175
7236
  const alibaba = (fn, model, apiKey) => {
7176
- const wrappedFn = async (args) => {
7237
+ const wrappedFn = async (...args) => {
7177
7238
  return await ContextService.runInContext(async () => {
7178
7239
  return await fn(...args);
7179
7240
  }, {
@@ -7184,26 +7245,6 @@ const alibaba = (fn, model, apiKey) => {
7184
7245
  };
7185
7246
  return wrappedFn;
7186
7247
  };
7187
- /**
7188
- * Wrap async function with Zhipu AI GLM-4 inference context.
7189
- *
7190
- * Creates a higher-order function that executes the provided async function
7191
- * within a Zhipu AI GLM-4 inference context via OpenAI-compatible Z.ai API.
7192
- *
7193
- * @template T - Async function type
7194
- * @param fn - Async function to wrap
7195
- * @param model - GLM-4 model name (e.g., "glm-4-plus", "glm-4-air")
7196
- * @param apiKey - Single API key or array of keys
7197
- * @returns Wrapped function with same signature as input
7198
- *
7199
- * @example
7200
- * ```typescript
7201
- * import { glm4 } from '@backtest-kit/ollama';
7202
- *
7203
- * const wrappedFn = glm4(myAsyncFn, 'glm-4-plus', process.env.ZAI_API_KEY);
7204
- * const result = await wrappedFn(args);
7205
- * ```
7206
- */
7207
7248
  /**
7208
7249
  * Wrap async function with Groq inference context.
7209
7250
  *
@@ -7225,7 +7266,7 @@ const alibaba = (fn, model, apiKey) => {
7225
7266
  * ```
7226
7267
  */
7227
7268
  const groq = (fn, model, apiKey) => {
7228
- const wrappedFn = async (args) => {
7269
+ const wrappedFn = async (...args) => {
7229
7270
  return await ContextService.runInContext(async () => {
7230
7271
  return await fn(...args);
7231
7272
  }, {
@@ -7236,8 +7277,28 @@ const groq = (fn, model, apiKey) => {
7236
7277
  };
7237
7278
  return wrappedFn;
7238
7279
  };
7280
+ /**
7281
+ * Wrap async function with Zhipu AI GLM-4 inference context.
7282
+ *
7283
+ * Creates a higher-order function that executes the provided async function
7284
+ * within a Zhipu AI GLM-4 inference context via OpenAI-compatible Z.ai API.
7285
+ *
7286
+ * @template T - Async function type
7287
+ * @param fn - Async function to wrap
7288
+ * @param model - GLM-4 model name (e.g., "glm-4-plus", "glm-4-air")
7289
+ * @param apiKey - Single API key or array of keys
7290
+ * @returns Wrapped function with same signature as input
7291
+ *
7292
+ * @example
7293
+ * ```typescript
7294
+ * import { glm4 } from '@backtest-kit/ollama';
7295
+ *
7296
+ * const wrappedFn = glm4(myAsyncFn, 'glm-4-plus', process.env.ZAI_API_KEY);
7297
+ * const result = await wrappedFn(args);
7298
+ * ```
7299
+ */
7239
7300
  const glm4 = (fn, model, apiKey) => {
7240
- const wrappedFn = async (args) => {
7301
+ const wrappedFn = async (...args) => {
7241
7302
  return await ContextService.runInContext(async () => {
7242
7303
  return await fn(...args);
7243
7304
  }, {
@@ -7290,7 +7351,8 @@ const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
7290
7351
  *
7291
7352
  * @example
7292
7353
  * ```typescript
7293
- * import { dumpSignal, getCandles } from "backtest-kit";
7354
+ * import { getCandles } from "backtest-kit";
7355
+ * import { dumpSignal } from "@backtest-kit/ollama";
7294
7356
  * import { v4 as uuid } from "uuid";
7295
7357
  *
7296
7358
  * addStrategy({
@@ -7437,7 +7499,9 @@ async function validate(args = {}) {
7437
7499
  return await validateInternal(args);
7438
7500
  }
7439
7501
 
7440
- const MODULE_TYPE_SYMBOL = Symbol("module-type");
7502
+ // Symbol.for keeps the brand check working when CJS and ESM copies of the
7503
+ // package end up in the same process (dual-package hazard).
7504
+ const MODULE_TYPE_SYMBOL = Symbol.for("backtest-kit.ollama.module-type");
7441
7505
  class Module {
7442
7506
  constructor(path, baseDir) {
7443
7507
  this.path = path;
@@ -7498,15 +7562,15 @@ async function commitPrompt(source, history) {
7498
7562
  const systemPrompts = await engine$1.resolvePromptService.getSystemPrompt(prompt, symbol, strategyName, exchangeName, frameName, isBacktest);
7499
7563
  const userPrompt = await engine$1.resolvePromptService.getUserPrompt(prompt, symbol, strategyName, exchangeName, frameName, isBacktest);
7500
7564
  if (systemPrompts.length > 0) {
7501
- for (const content of systemPrompts) {
7502
- if (!content.trim()) {
7503
- continue;
7504
- }
7505
- history.unshift({
7506
- role: "system",
7507
- content,
7508
- });
7509
- }
7565
+ // Single unshift with spread: per-item unshift in a loop would REVERSE
7566
+ // the system prompt order at the head of the history.
7567
+ const systemMessages = systemPrompts
7568
+ .filter((content) => content.trim())
7569
+ .map((content) => ({
7570
+ role: "system",
7571
+ content,
7572
+ }));
7573
+ history.unshift(...systemMessages);
7510
7574
  }
7511
7575
  if (userPrompt && userPrompt.trim()) {
7512
7576
  history.push({
@@ -7686,7 +7750,7 @@ const LIST_OPTIMIZERS_METHOD_NAME = "list.listOptimizerSchema";
7686
7750
  *
7687
7751
  * @example
7688
7752
  * ```typescript
7689
- * import { listOptimizers, addOptimizer } from "backtest-kit";
7753
+ * import { listOptimizers, addOptimizer } from "@backtest-kit/ollama";
7690
7754
  *
7691
7755
  * addOptimizer({
7692
7756
  * optimizerName: "llm-strategy-generator",