@cubicecho/agent-core 2.1.1 → 2.1.2

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 (2) hide show
  1. package/dist/side-task.js +37 -12
  2. package/package.json +1 -1
package/dist/side-task.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import OpenAI from "openai";
2
+ import { capabilitiesFor, negotiate } from "./capabilities.js";
2
3
  import { getClient } from "./client.js";
3
4
  import { errorMessage } from "./errors.js";
4
5
  import { isTransient } from "./retry.js";
@@ -13,11 +14,13 @@ import { isTransient } from "./retry.js";
13
14
  * the OpenAI-compatible spelling and `chat_template_kwargs` the llama.cpp/vLLM one; servers
14
15
  * disagree about which they take, so send both. One that rejects the unknown fields gets a
15
16
  * single retry without them, and is not offered them again.
17
+ *
18
+ * Only the second half is latched here. `reasoning_effort` is a field `negotiate` already knows
19
+ * how to be refused, so it is sent under `ModelCapabilities.reasoningEffort` instead — which
20
+ * both narrows the fallback below to the field it is really about, and shares the answer with
21
+ * the runs on that model rather than keeping a second opinion about it.
16
22
  */
17
- const NO_THINKING = {
18
- reasoning_effort: "none",
19
- chat_template_kwargs: { enable_thinking: false },
20
- };
23
+ const NO_THINKING = { chat_template_kwargs: { enable_thinking: false } };
21
24
  /**
22
25
  * The models that turned out not to take the hints, by endpoint and model.
23
26
  *
@@ -27,9 +30,12 @@ const NO_THINKING = {
27
30
  * second from ever being asked.
28
31
  *
29
32
  * The model belongs in the key for the same reason. One base URL is routinely many models —
30
- * OpenRouter, LiteLLM, vLLM serving several at once — and whether `reasoning_effort` is
31
- * understood is a property of the model behind the route, not of the route. Keyed on the host
32
- * alone, the first model to refuse spoke for every model on it.
33
+ * OpenRouter, LiteLLM, vLLM serving several at once — and whether `chat_template_kwargs` reaches
34
+ * a chat template that reads it is a property of the model behind the route, not of the route.
35
+ * Keyed on the host alone, the first model to refuse spoke for every model on it.
36
+ *
37
+ * Only the `chat_template_kwargs` half is here. `reasoning_effort` is `negotiate`'s to latch, on
38
+ * the same (endpoint, model) pair, where a run on that model can read it too.
33
39
  */
34
40
  const noHints = new Set();
35
41
  const hintKey = (baseUrl, model) => JSON.stringify([baseUrl, model]);
@@ -81,28 +87,47 @@ const stripThinking = (text) => text
81
87
  * @param options Reply ceiling, temperature, cancellation, notices.
82
88
  */
83
89
  export async function ask(config, model, system, user, { maxTokens = 512, temperature = 0.3, signal, onNotice } = {}) {
84
- const send = (hints) => getClient(config).chat.completions.create({
90
+ const send = (hints, refused) => getClient(config).chat.completions.create({
85
91
  model,
86
- max_tokens: maxTokens,
87
- temperature,
92
+ // The reasoning models want the ceiling spelled the other way, and they are exactly the
93
+ // models a side task most wants to stop deliberating.
94
+ ...(refused && !refused.legacyTokenLimit
95
+ ? { max_completion_tokens: maxTokens }
96
+ : { max_tokens: maxTokens }),
97
+ // One that will only run at the temperature it was built with is sent none: a side task
98
+ // wants the same answer twice, and 1.0 from that model is as close as it gets.
99
+ ...(refused && !refused.chosenTemperature ? {} : { temperature }),
88
100
  messages: [
89
101
  { role: "system", content: system },
90
102
  { role: "user", content: user },
91
103
  ],
92
104
  ...(hints ? NO_THINKING : {}),
105
+ ...(hints && refused?.reasoningEffort !== false ? { reasoning_effort: "none" } : {}),
93
106
  }, { signal });
107
+ // The endpoint's own object, not one of this module's: what a model refuses is the same fact
108
+ // whether a run or a side task found it out, and the point of latching it is that only one of
109
+ // them has to pay for it. Nothing here sends tools or `stream_options`, so the two
110
+ // endpoint-level flags are not in play — the model's three are the whole of what this meets.
111
+ const supports = capabilitiesFor(config.baseUrl);
112
+ const attempt = (hints) => negotiate(supports, (_supports, _produced, refused) => send(hints, refused), {
113
+ model,
114
+ onNotice,
115
+ });
94
116
  const key = hintKey(config.baseUrl, model);
95
117
  const hints = !noHints.has(key);
96
118
  let response;
97
119
  try {
98
- response = await send(hints);
120
+ response = await attempt(hints);
99
121
  }
100
122
  catch (error) {
123
+ // Whatever is left after `negotiate` has answered everything it knows: on this path that is
124
+ // the hints it does not, which is `chat_template_kwargs` and an effort the model has but
125
+ // does not offer as `none`.
101
126
  if (!hints || !rejectedTheRequest(error))
102
127
  throw error;
103
128
  onNotice?.("server rejected the no-thinking hints; retrying without them");
104
129
  noHints.add(key);
105
- response = await send(false);
130
+ response = await attempt(false);
106
131
  }
107
132
  const message = response.choices[0]?.message;
108
133
  const answer = stripThinking(message?.content ?? "").trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubicecho/agent-core",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "The endpoint-agnostic half of an OpenAI-compatible agent loop: tool-schema compatibility, on-demand tool loading, one-shot side tasks, run events, and a pooled client.",
5
5
  "keywords": [
6
6
  "openai",