@tyvm/knowhow 0.0.145 → 0.0.146
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/package.json +1 -1
- package/src/chat/CliChatService.ts +16 -0
- package/src/chat/modules/AgentModule.ts +30 -23
- package/src/chat/modules/SessionsModule.ts +7 -5
- package/src/chat/types.ts +3 -2
- package/src/clients/anthropic.ts +24 -0
- package/src/clients/fireworks.ts +3 -0
- package/src/clients/gemini.ts +8 -0
- package/src/clients/openai.ts +7 -4
- package/src/clients/types.ts +30 -7
- package/src/clients/xai.ts +23 -4
- package/src/commands/agent.ts +10 -1
- package/src/commands/services.ts +49 -3
- package/src/plugins/plugins.ts +16 -0
- package/src/processors/TokenCompressor.ts +40 -8
- package/src/processors/ToolResponseCache.ts +17 -8
- package/src/processors/tools/boundedToolResponse.ts +49 -0
- package/src/processors/tools/grepToolResponse.ts +164 -38
- package/src/processors/tools/index.ts +2 -0
- package/src/processors/tools/jqToolResponse.ts +40 -10
- package/src/processors/tools/listStoredToolResponses.ts +51 -10
- package/src/processors/tools/tailToolResponse.ts +85 -17
- package/src/services/LazyToolsService.ts +7 -0
- package/src/services/Mcp.ts +9 -2
- package/src/services/RuntimeReloadService.ts +31 -0
- package/src/services/Tools.ts +11 -0
- package/src/services/TracingService.ts +116 -0
- package/src/services/index.ts +8 -0
- package/src/services/modules/index.ts +45 -2
- package/src/services/modules/types.ts +8 -0
- package/src/util/Trace.ts +236 -0
- package/tests/clients/fireworks.test.ts +65 -0
- package/tests/clients/reasoningEffort.test.ts +79 -0
- package/tests/compressor/toolResponseCache.test.ts +2 -1
- package/tests/manual/clients/completions.test.ts +27 -10
- package/tests/processors/TokenCompressor.test.ts +17 -3
- package/tests/processors/tools/boundedRetrievalTools.test.ts +68 -0
- package/tests/processors/tools/grepToolResponse.test.ts +88 -1
- package/tests/unit/plugins/pluginLoading.test.ts +31 -0
- package/tests/unit/services/RuntimeReloadService.test.ts +29 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/chat/CliChatService.js +15 -0
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +13 -19
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.js +1 -6
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/chat/types.d.ts +3 -2
- package/ts_build/src/clients/anthropic.d.ts +1 -0
- package/ts_build/src/clients/anthropic.js +12 -0
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/fireworks.js +1 -0
- package/ts_build/src/clients/fireworks.js.map +1 -1
- package/ts_build/src/clients/gemini.js +8 -0
- package/ts_build/src/clients/gemini.js.map +1 -1
- package/ts_build/src/clients/openai.js +4 -2
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +4 -1
- package/ts_build/src/clients/types.js +14 -0
- package/ts_build/src/clients/types.js.map +1 -1
- package/ts_build/src/clients/xai.d.ts +1 -0
- package/ts_build/src/clients/xai.js +23 -4
- package/ts_build/src/clients/xai.js.map +1 -1
- package/ts_build/src/commands/agent.js +6 -1
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/services.d.ts +2 -0
- package/ts_build/src/commands/services.js +45 -3
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/plugins/plugins.d.ts +1 -0
- package/ts_build/src/plugins/plugins.js +14 -0
- package/ts_build/src/plugins/plugins.js.map +1 -1
- package/ts_build/src/processors/TokenCompressor.js +15 -6
- package/ts_build/src/processors/TokenCompressor.js.map +1 -1
- package/ts_build/src/processors/ToolResponseCache.d.ts +3 -3
- package/ts_build/src/processors/ToolResponseCache.js +8 -8
- package/ts_build/src/processors/ToolResponseCache.js.map +1 -1
- package/ts_build/src/processors/tools/boundedToolResponse.d.ts +7 -0
- package/ts_build/src/processors/tools/boundedToolResponse.js +27 -0
- package/ts_build/src/processors/tools/boundedToolResponse.js.map +1 -0
- package/ts_build/src/processors/tools/grepToolResponse.d.ts +4 -0
- package/ts_build/src/processors/tools/grepToolResponse.js +101 -25
- package/ts_build/src/processors/tools/grepToolResponse.js.map +1 -1
- package/ts_build/src/processors/tools/index.d.ts +2 -2
- package/ts_build/src/processors/tools/index.js.map +1 -1
- package/ts_build/src/processors/tools/jqToolResponse.d.ts +5 -1
- package/ts_build/src/processors/tools/jqToolResponse.js +26 -9
- package/ts_build/src/processors/tools/jqToolResponse.js.map +1 -1
- package/ts_build/src/processors/tools/listStoredToolResponses.d.ts +7 -1
- package/ts_build/src/processors/tools/listStoredToolResponses.js +39 -9
- package/ts_build/src/processors/tools/listStoredToolResponses.js.map +1 -1
- package/ts_build/src/processors/tools/tailToolResponse.d.ts +5 -0
- package/ts_build/src/processors/tools/tailToolResponse.js +56 -13
- package/ts_build/src/processors/tools/tailToolResponse.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.d.ts +1 -0
- package/ts_build/src/services/LazyToolsService.js +5 -0
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/Mcp.js +8 -2
- package/ts_build/src/services/Mcp.js.map +1 -1
- package/ts_build/src/services/RuntimeReloadService.d.ts +12 -0
- package/ts_build/src/services/RuntimeReloadService.js +26 -0
- package/ts_build/src/services/RuntimeReloadService.js.map +1 -0
- package/ts_build/src/services/Tools.d.ts +1 -0
- package/ts_build/src/services/Tools.js +5 -0
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/TracingService.d.ts +16 -0
- package/ts_build/src/services/TracingService.js +35 -0
- package/ts_build/src/services/TracingService.js.map +1 -0
- package/ts_build/src/services/index.d.ts +6 -0
- package/ts_build/src/services/index.js +7 -1
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +5 -0
- package/ts_build/src/services/modules/index.js +28 -2
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/services/modules/types.d.ts +3 -0
- package/ts_build/src/util/Trace.d.ts +13 -0
- package/ts_build/src/util/Trace.js +107 -0
- package/ts_build/src/util/Trace.js.map +1 -0
- package/ts_build/tests/clients/fireworks.test.d.ts +1 -0
- package/ts_build/tests/clients/fireworks.test.js +65 -0
- package/ts_build/tests/clients/fireworks.test.js.map +1 -0
- package/ts_build/tests/clients/reasoningEffort.test.d.ts +1 -0
- package/ts_build/tests/clients/reasoningEffort.test.js +68 -0
- package/ts_build/tests/clients/reasoningEffort.test.js.map +1 -0
- package/ts_build/tests/compressor/toolResponseCache.test.js +1 -1
- package/ts_build/tests/compressor/toolResponseCache.test.js.map +1 -1
- package/ts_build/tests/manual/clients/completions.test.js +19 -6
- package/ts_build/tests/manual/clients/completions.test.js.map +1 -1
- package/ts_build/tests/processors/TokenCompressor.test.js +11 -2
- package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
- package/ts_build/tests/processors/tools/boundedRetrievalTools.test.d.ts +1 -0
- package/ts_build/tests/processors/tools/boundedRetrievalTools.test.js +57 -0
- package/ts_build/tests/processors/tools/boundedRetrievalTools.test.js.map +1 -0
- package/ts_build/tests/processors/tools/grepToolResponse.test.js +37 -0
- package/ts_build/tests/processors/tools/grepToolResponse.test.js.map +1 -1
- package/ts_build/tests/unit/plugins/pluginLoading.test.js +59 -0
- package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
- package/ts_build/tests/unit/services/RuntimeReloadService.test.d.ts +1 -0
- package/ts_build/tests/unit/services/RuntimeReloadService.test.js +26 -0
- package/ts_build/tests/unit/services/RuntimeReloadService.test.js.map +1 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace.ts — Method / class decorators and `tracify()` helper for the
|
|
3
|
+
* @tyvm/knowhow package.
|
|
4
|
+
*
|
|
5
|
+
* These decorators are zero-cost when no tracer is registered (the default).
|
|
6
|
+
* Once `@tyvm/knowhow-module-tracing` (or any other module) calls
|
|
7
|
+
* `TracingService.register(impl)`, every decorated method starts emitting
|
|
8
|
+
* real spans automatically — no code changes required.
|
|
9
|
+
*
|
|
10
|
+
* API mirrors the backend's `Trace.ts` so patterns learned there carry over:
|
|
11
|
+
*
|
|
12
|
+
* @Trace() — wrap a single method
|
|
13
|
+
* @TraceAll() — wrap every public method on a class
|
|
14
|
+
* tracify(obj, ns) — wrap an already-constructed instance
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* import { Trace, TraceAll, tracify } from '../util/Trace';
|
|
18
|
+
*
|
|
19
|
+
* @TraceAll()
|
|
20
|
+
* export class MessageProcessor {
|
|
21
|
+
* async process(msg: Message) { ... } // → span "MessageProcessor.process"
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* @Trace()
|
|
25
|
+
* async fetchEmbeddings(text: string) { ... } // → span "ClassName.fetchEmbeddings"
|
|
26
|
+
*
|
|
27
|
+
* export const myClient = tracify(new SomeClient(), 'someClient');
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { TracingService } from "../services/TracingService";
|
|
31
|
+
|
|
32
|
+
// ─── Shared span lifecycle helper ─────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function runWithSpan<T>(
|
|
35
|
+
spanName: string,
|
|
36
|
+
attributes: Record<string, string | number | boolean>,
|
|
37
|
+
fn: () => T
|
|
38
|
+
): T {
|
|
39
|
+
const span = TracingService.startSpan(spanName, attributes);
|
|
40
|
+
|
|
41
|
+
const finish = (err?: unknown) => {
|
|
42
|
+
if (err !== undefined) {
|
|
43
|
+
span.recordError(err);
|
|
44
|
+
}
|
|
45
|
+
span.end();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const result = TracingService.withSpan(span, fn);
|
|
50
|
+
|
|
51
|
+
if (result instanceof Promise) {
|
|
52
|
+
return result.then(
|
|
53
|
+
(value) => {
|
|
54
|
+
finish();
|
|
55
|
+
return value;
|
|
56
|
+
},
|
|
57
|
+
(err) => {
|
|
58
|
+
finish(err);
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
) as unknown as T;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
finish();
|
|
65
|
+
return result;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
finish(err);
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── @Trace() — single-method decorator ──────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export interface TraceOptions {
|
|
75
|
+
/** Override the span name (default: `ClassName.methodName`) */
|
|
76
|
+
spanName?: string;
|
|
77
|
+
/** Additional static attributes attached to every span */
|
|
78
|
+
attributes?: Record<string, string | number | boolean>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @Trace() method decorator — wraps the decorated method in a tracing span.
|
|
83
|
+
*
|
|
84
|
+
* Span name defaults to `ClassName.methodName`. Errors are recorded on the
|
|
85
|
+
* span and re-thrown. Works with both sync and async methods.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* @Trace()
|
|
89
|
+
* async fetchEmbeddings(text: string) { ... }
|
|
90
|
+
*
|
|
91
|
+
* @Trace({ spanName: 'embeddings.fetch', attributes: { 'peer.service': 'openai' } })
|
|
92
|
+
* async fetchEmbeddings(text: string) { ... }
|
|
93
|
+
*/
|
|
94
|
+
export function Trace(options: TraceOptions = {}) {
|
|
95
|
+
return function (
|
|
96
|
+
target: object,
|
|
97
|
+
propertyKey: string,
|
|
98
|
+
descriptor: PropertyDescriptor
|
|
99
|
+
): PropertyDescriptor {
|
|
100
|
+
const originalMethod = descriptor.value as (...args: unknown[]) => unknown;
|
|
101
|
+
const className = (target as any).constructor?.name ?? "Unknown";
|
|
102
|
+
const resolvedSpanName =
|
|
103
|
+
options.spanName ?? `${className}.${propertyKey}`;
|
|
104
|
+
const staticAttrs: Record<string, string | number | boolean> = {
|
|
105
|
+
"code.function": propertyKey,
|
|
106
|
+
"code.namespace": className,
|
|
107
|
+
...options.attributes,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
descriptor.value = function (this: unknown, ...args: unknown[]) {
|
|
111
|
+
return runWithSpan(resolvedSpanName, staticAttrs, () =>
|
|
112
|
+
originalMethod.apply(this, args)
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return descriptor;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── @TraceAll() — class decorator ───────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export interface TraceAllOptions {
|
|
123
|
+
/** Additional static attributes attached to every span */
|
|
124
|
+
attributes?: Record<string, string | number | boolean>;
|
|
125
|
+
/** Method names to skip (in addition to constructor and `_`-prefixed) */
|
|
126
|
+
skip?: string[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @TraceAll() class decorator — wraps every public method on the class in a
|
|
131
|
+
* tracing span named `ClassName.methodName`.
|
|
132
|
+
*
|
|
133
|
+
* Skips the constructor and any methods whose name starts with `_`.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* @TraceAll()
|
|
137
|
+
* export class MessageProcessor {
|
|
138
|
+
* async process(msg: Message) { ... } // → span "MessageProcessor.process"
|
|
139
|
+
* }
|
|
140
|
+
*/
|
|
141
|
+
export function TraceAll(options: TraceAllOptions = {}) {
|
|
142
|
+
const skip = new Set(options.skip ?? []);
|
|
143
|
+
|
|
144
|
+
return function <T extends { new (...args: unknown[]): object }>(
|
|
145
|
+
constructor: T
|
|
146
|
+
): T {
|
|
147
|
+
const className = constructor.name;
|
|
148
|
+
const proto = constructor.prototype as Record<string, unknown>;
|
|
149
|
+
|
|
150
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
151
|
+
if (key === "constructor") continue;
|
|
152
|
+
if (key.startsWith("_")) continue;
|
|
153
|
+
if (skip.has(key)) continue;
|
|
154
|
+
|
|
155
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
|
|
156
|
+
if (!descriptor || typeof descriptor.value !== "function") continue;
|
|
157
|
+
|
|
158
|
+
const originalMethod = descriptor.value as (
|
|
159
|
+
...args: unknown[]
|
|
160
|
+
) => unknown;
|
|
161
|
+
const spanName = `${className}.${key}`;
|
|
162
|
+
const staticAttrs: Record<string, string | number | boolean> = {
|
|
163
|
+
"code.function": key,
|
|
164
|
+
"code.namespace": className,
|
|
165
|
+
...options.attributes,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
descriptor.value = function (this: unknown, ...args: unknown[]) {
|
|
169
|
+
return runWithSpan(spanName, staticAttrs, () =>
|
|
170
|
+
originalMethod.apply(this, args)
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
Object.defineProperty(proto, key, descriptor);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return constructor;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── tracify() — instance-level wrapper ──────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* tracify — wraps every method on an already-constructed object instance in a
|
|
185
|
+
* tracing span.
|
|
186
|
+
*
|
|
187
|
+
* Ideal for third-party singletons (SDK clients etc.) that cannot be decorated
|
|
188
|
+
* at the class level.
|
|
189
|
+
*
|
|
190
|
+
* @param instance The object whose methods should be traced.
|
|
191
|
+
* @param namespace Prefix for span names, e.g. "embeddingsClient".
|
|
192
|
+
* @param options Optional extra attributes / skip list.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* export const embeddingsClient = tracify(new OpenAI(), 'openai');
|
|
196
|
+
*/
|
|
197
|
+
export function tracify<T extends object>(
|
|
198
|
+
instance: T,
|
|
199
|
+
namespace: string,
|
|
200
|
+
options: TraceAllOptions = {}
|
|
201
|
+
): T {
|
|
202
|
+
const skip = new Set(options.skip ?? []);
|
|
203
|
+
|
|
204
|
+
return new Proxy(instance, {
|
|
205
|
+
get(target, prop, receiver) {
|
|
206
|
+
const value = Reflect.get(target, prop, receiver);
|
|
207
|
+
const methodName = typeof prop === "string" ? prop : String(prop);
|
|
208
|
+
|
|
209
|
+
// Pass symbols through unwrapped
|
|
210
|
+
if (typeof prop === "symbol") return value;
|
|
211
|
+
|
|
212
|
+
// Skip private / user-skipped names
|
|
213
|
+
if (methodName.startsWith("_") || skip.has(methodName)) return value;
|
|
214
|
+
|
|
215
|
+
// Recursively wrap nested objects (e.g. prisma.user.findMany)
|
|
216
|
+
if (value !== null && typeof value === "object") {
|
|
217
|
+
return tracify(value as object, `${namespace}.${methodName}`, options);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (typeof value !== "function") return value;
|
|
221
|
+
|
|
222
|
+
const spanName = `${namespace}.${methodName}`;
|
|
223
|
+
const staticAttrs: Record<string, string | number | boolean> = {
|
|
224
|
+
"code.function": methodName,
|
|
225
|
+
"code.namespace": namespace,
|
|
226
|
+
...options.attributes,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return function (this: unknown, ...args: unknown[]) {
|
|
230
|
+
return runWithSpan(spanName, staticAttrs, () =>
|
|
231
|
+
(value as Function).apply(target, args)
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import http from "../../src/utils/http";
|
|
2
|
+
import { GenericFireworksClient } from "../../src/clients/fireworks";
|
|
3
|
+
import { FireworksModels } from "../../src/clients/pricing/fireworks";
|
|
4
|
+
|
|
5
|
+
jest.mock("../../src/utils/http", () => ({
|
|
6
|
+
__esModule: true,
|
|
7
|
+
default: { post: jest.fn() },
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
const post = http.post as jest.MockedFunction<typeof http.post>;
|
|
11
|
+
|
|
12
|
+
describe("GenericFireworksClient", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
post.mockReset();
|
|
15
|
+
post.mockResolvedValue({
|
|
16
|
+
data: {
|
|
17
|
+
choices: [{ message: { role: "assistant", content: "ok" } }],
|
|
18
|
+
model: FireworksModels.DeepseekV4Flash,
|
|
19
|
+
usage: { prompt_tokens: 2, completion_tokens: 1, total_tokens: 3 },
|
|
20
|
+
},
|
|
21
|
+
} as any);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("removes non-Fireworks options and tool return metadata", async () => {
|
|
25
|
+
const client = new GenericFireworksClient("test-key");
|
|
26
|
+
|
|
27
|
+
await client.createChatCompletion({
|
|
28
|
+
model: FireworksModels.DeepseekV4Flash,
|
|
29
|
+
messages: [{ role: "user", content: "hello" }],
|
|
30
|
+
max_tokens: 100,
|
|
31
|
+
reasoning_summary: true,
|
|
32
|
+
long_ttl_cache: true,
|
|
33
|
+
tools: [{
|
|
34
|
+
type: "function",
|
|
35
|
+
function: {
|
|
36
|
+
name: "lookup",
|
|
37
|
+
description: "Look something up",
|
|
38
|
+
parameters: { type: "object", properties: {} },
|
|
39
|
+
returns: { type: "string" },
|
|
40
|
+
} as any,
|
|
41
|
+
}],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(post).toHaveBeenCalledTimes(1);
|
|
45
|
+
const [url, body, requestOptions] = post.mock.calls[0];
|
|
46
|
+
expect(url).toBe("https://api.fireworks.ai/inference/v1/chat/completions");
|
|
47
|
+
expect(requestOptions).toMatchObject({
|
|
48
|
+
headers: { Authorization: "Bearer test-key" },
|
|
49
|
+
});
|
|
50
|
+
expect(body).toMatchObject({
|
|
51
|
+
model: FireworksModels.DeepseekV4Flash,
|
|
52
|
+
messages: [{ role: "user", content: "hello" }],
|
|
53
|
+
max_tokens: 100,
|
|
54
|
+
tool_choice: "auto",
|
|
55
|
+
});
|
|
56
|
+
const serializedBody = JSON.parse(JSON.stringify(body));
|
|
57
|
+
expect(serializedBody).not.toHaveProperty("reasoning_summary");
|
|
58
|
+
expect(serializedBody).not.toHaveProperty("long_ttl_cache");
|
|
59
|
+
expect(serializedBody.tools[0].function).toEqual({
|
|
60
|
+
name: "lookup",
|
|
61
|
+
description: "Look something up",
|
|
62
|
+
parameters: { type: "object", properties: {} },
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { GenericAnthropicClient } from "../../src/clients/anthropic";
|
|
2
|
+
import { GenericGeminiClient } from "../../src/clients/gemini";
|
|
3
|
+
import { GenericOpenAiClient } from "../../src/clients/openai";
|
|
4
|
+
import { AnthropicModels } from "../../src/clients/pricing";
|
|
5
|
+
import {
|
|
6
|
+
REASONING_EFFORTS,
|
|
7
|
+
ReasoningEffort,
|
|
8
|
+
isReasoningEffort,
|
|
9
|
+
} from "../../src/clients/types";
|
|
10
|
+
import {
|
|
11
|
+
GoogleThinkingBudgetModels,
|
|
12
|
+
GoogleThinkingLevelModels,
|
|
13
|
+
} from "../../src/types";
|
|
14
|
+
import { GenericXAIClient } from "../../src/clients/xai";
|
|
15
|
+
|
|
16
|
+
const options = (model: string, reasoning_effort: ReasoningEffort) => ({
|
|
17
|
+
model,
|
|
18
|
+
reasoning_effort,
|
|
19
|
+
messages: [],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("reasoning effort values", () => {
|
|
23
|
+
it("accepts the complete provider-neutral set and rejects other values", () => {
|
|
24
|
+
expect(REASONING_EFFORTS).toEqual([
|
|
25
|
+
"none",
|
|
26
|
+
"minimal",
|
|
27
|
+
"low",
|
|
28
|
+
"medium",
|
|
29
|
+
"high",
|
|
30
|
+
"xhigh",
|
|
31
|
+
"max",
|
|
32
|
+
]);
|
|
33
|
+
REASONING_EFFORTS.forEach((effort) => expect(isReasoningEffort(effort)).toBe(true));
|
|
34
|
+
expect(isReasoningEffort("extra-high")).toBe(false);
|
|
35
|
+
expect(isReasoningEffort(undefined)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("maps provider-neutral values to OpenAI legacy and Responses spellings", () => {
|
|
39
|
+
const client = new GenericOpenAiClient("fake-key");
|
|
40
|
+
expect(client.resolveReasoningEffort(options("unknown", "none"))).toBe("low");
|
|
41
|
+
expect(client.resolveReasoningEffort(options("unknown", "minimal"))).toBe("low");
|
|
42
|
+
expect(client.resolveReasoningEffort(options("unknown", "xhigh"))).toBe("high");
|
|
43
|
+
expect(client.resolveReasoningEffort(options("unknown", "max"))).toBe("high");
|
|
44
|
+
expect(client.resolveReasoningEffortForModel(options("unknown", "max"))).toBe("xhigh");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("maps all values to valid Gemini thinking levels and budgets", () => {
|
|
48
|
+
const client = new GenericGeminiClient("fake-key");
|
|
49
|
+
const levelModel = GoogleThinkingLevelModels[0];
|
|
50
|
+
const budgetModel = GoogleThinkingBudgetModels[0];
|
|
51
|
+
|
|
52
|
+
expect(client.buildThinkingConfig(options(levelModel, "none"))).toEqual({ thinkingLevel: "minimal" });
|
|
53
|
+
expect(client.buildThinkingConfig(options(levelModel, "minimal"))).toEqual({ thinkingLevel: "minimal" });
|
|
54
|
+
expect(client.buildThinkingConfig(options(levelModel, "xhigh"))).toEqual({ thinkingLevel: "high" });
|
|
55
|
+
expect(client.buildThinkingConfig(options(levelModel, "max"))).toEqual({ thinkingLevel: "high" });
|
|
56
|
+
expect(client.buildThinkingConfig(options(budgetModel, "none"))).toEqual({ thinkingBudget: 0 });
|
|
57
|
+
expect(client.buildThinkingConfig(options(budgetModel, "minimal"))).toEqual({ thinkingBudget: 512 });
|
|
58
|
+
expect(client.buildThinkingConfig(options(budgetModel, "medium"))).toEqual({ thinkingBudget: 8192 });
|
|
59
|
+
expect(client.buildThinkingConfig(options(budgetModel, "max"))).toEqual({ thinkingBudget: -1 });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("maps provider-neutral values to Anthropic output effort", () => {
|
|
63
|
+
const client = new GenericAnthropicClient("fake-key");
|
|
64
|
+
expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "none")).toBeUndefined();
|
|
65
|
+
expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "minimal")).toEqual({ effort: "low" });
|
|
66
|
+
expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "xhigh")).toEqual({ effort: "xhigh" });
|
|
67
|
+
expect(client.getEffortOutputConfig(AnthropicModels.Sonnet5, "max")).toEqual({ effort: "max" });
|
|
68
|
+
expect(client.getEffortOutputConfig("unsupported", "high")).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("maps provider-neutral values to xAI legacy effort", () => {
|
|
72
|
+
const client = new GenericXAIClient("fake-key");
|
|
73
|
+
expect(client.resolveLegacyReasoningEffort("none")).toBeUndefined();
|
|
74
|
+
expect(client.resolveLegacyReasoningEffort("minimal")).toBe("low");
|
|
75
|
+
expect(client.resolveLegacyReasoningEffort("medium")).toBe("medium");
|
|
76
|
+
expect(client.resolveLegacyReasoningEffort("xhigh")).toBe("high");
|
|
77
|
+
expect(client.resolveLegacyReasoningEffort("max")).toBe("high");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -111,7 +111,8 @@ describe("ToolResponseCache - MCP Format Support", () => {
|
|
|
111
111
|
it("should filter data array with ._data | map(select(.private == true))", async () => {
|
|
112
112
|
const result = await cache.queryToolResponse(
|
|
113
113
|
"github-repos",
|
|
114
|
-
"._data | map(select(.private == true))"
|
|
114
|
+
"._data | map(select(.private == true))",
|
|
115
|
+
{ maxCharacters: 50_000 }
|
|
115
116
|
);
|
|
116
117
|
|
|
117
118
|
// Parse and verify filtering worked
|
|
@@ -17,6 +17,7 @@ import { OpenAiChatModels, OpenAiReasoningModels, OpenAiResponsesOnlyModels } fr
|
|
|
17
17
|
import { AnthropicTextModels, AnthropicLimitedAvailabilityModels } from "../../../src/clients/pricing/anthropic";
|
|
18
18
|
import { GoogleTextModels } from "../../../src/clients/pricing/google";
|
|
19
19
|
import { XaiTextModels, XaiDeprecatedTextModels } from "../../../src/clients/pricing/xai";
|
|
20
|
+
import { FireworksTextPricing } from "../../../src/clients/pricing/fireworks";
|
|
20
21
|
|
|
21
22
|
const OUTPUT_FILE = path.join(__dirname, "completions.json");
|
|
22
23
|
const PROMPT = "Say hello in 5 words.";
|
|
@@ -29,6 +30,7 @@ interface ModelEntry {
|
|
|
29
30
|
provider: string;
|
|
30
31
|
model: string;
|
|
31
32
|
envKey: string;
|
|
33
|
+
reasoningEffort?: "low";
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
function openaiEntries(models: string[]): ModelEntry[] {
|
|
@@ -47,19 +49,35 @@ function xaiEntries(models: string[]): ModelEntry[] {
|
|
|
47
49
|
return models.map((model) => ({ provider: "xai", model, envKey: "XAI_API_KEY" }));
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
function fireworksEntries(): ModelEntry[] {
|
|
53
|
+
return Object.entries(FireworksTextPricing)
|
|
54
|
+
.filter(([, pricing]) => !pricing.deprecated)
|
|
55
|
+
.map(([model]) => ({
|
|
56
|
+
provider: "fireworks",
|
|
57
|
+
model,
|
|
58
|
+
envKey: "FIREWORKS_API_KEY",
|
|
59
|
+
// Fireworks model support for reasoning_effort is model-specific. This
|
|
60
|
+
// suite exercises the request shape used by Patcher without that flag.
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
const TEST_MODELS: ModelEntry[] = [
|
|
51
65
|
// OpenAI — all active chat + reasoning models + responses-API models
|
|
52
66
|
// Deduplicate since some models appear in multiple lists
|
|
53
|
-
...openaiEntries([...new Set([...OpenAiChatModels, ...OpenAiReasoningModels, ...OpenAiResponsesOnlyModels])])
|
|
67
|
+
...openaiEntries([...new Set([...OpenAiChatModels, ...OpenAiReasoningModels, ...OpenAiResponsesOnlyModels])])
|
|
68
|
+
.map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
|
|
54
69
|
|
|
55
70
|
// Anthropic — active text models (limited availability excluded)
|
|
56
|
-
...anthropicEntries(AnthropicTextModels),
|
|
71
|
+
...anthropicEntries(AnthropicTextModels).map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
|
|
57
72
|
|
|
58
73
|
// Google — all active text models
|
|
59
|
-
...googleEntries(GoogleTextModels),
|
|
74
|
+
...googleEntries(GoogleTextModels).map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
|
|
60
75
|
|
|
61
76
|
// XAI — active text models (deprecated grok-2 models excluded)
|
|
62
|
-
...xaiEntries(XaiTextModels),
|
|
77
|
+
...xaiEntries(XaiTextModels).map((entry) => ({ ...entry, reasoningEffort: "low" as const })),
|
|
78
|
+
|
|
79
|
+
// Fireworks — all active catalog models
|
|
80
|
+
...fireworksEntries(),
|
|
63
81
|
];
|
|
64
82
|
|
|
65
83
|
// ─── Persistence helpers ──────────────────────────────────────────────────────
|
|
@@ -118,7 +136,7 @@ describe("Text Completions – multi-provider model verification", () => {
|
|
|
118
136
|
console.log(` Total records: ${Object.keys(results).length}`);
|
|
119
137
|
});
|
|
120
138
|
|
|
121
|
-
for (const { provider, model, envKey } of TEST_MODELS) {
|
|
139
|
+
for (const { provider, model, envKey, reasoningEffort } of TEST_MODELS) {
|
|
122
140
|
const key = recordKey(provider, model);
|
|
123
141
|
|
|
124
142
|
test(`${provider} / ${model}`, async () => {
|
|
@@ -145,11 +163,10 @@ describe("Text Completions – multi-provider model verification", () => {
|
|
|
145
163
|
model,
|
|
146
164
|
messages: [{ role: "user", content: PROMPT }],
|
|
147
165
|
max_tokens: 2000,
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
reasoning_effort: "low",
|
|
166
|
+
// Exercise the summary option enabled by Patcher. Provider clients must
|
|
167
|
+
// translate or remove this internal option rather than forwarding it.
|
|
168
|
+
reasoning_summary: true,
|
|
169
|
+
reasoning_effort: reasoningEffort,
|
|
153
170
|
});
|
|
154
171
|
const durationMs = Date.now() - startMs;
|
|
155
172
|
|
|
@@ -446,7 +446,7 @@ describe("TokenCompressor", () => {
|
|
|
446
446
|
expect(result).toContain("Available keys:");
|
|
447
447
|
});
|
|
448
448
|
|
|
449
|
-
it("should auto-stitch a multi-chunk chain
|
|
449
|
+
it("should auto-stitch and paginate a multi-chunk chain", () => {
|
|
450
450
|
// Force a multi-chunk compression then expand the first key.
|
|
451
451
|
const content = "a".repeat(40000);
|
|
452
452
|
const placeholder = tokenCompressor.compressStringInChunks(content);
|
|
@@ -456,9 +456,23 @@ describe("TokenCompressor", () => {
|
|
|
456
456
|
const functions = toolsServiceCalls[0][0];
|
|
457
457
|
const result = functions.expandTokens(firstKey);
|
|
458
458
|
|
|
459
|
-
|
|
460
|
-
expect(result).
|
|
459
|
+
expect(result.length).toBeLessThanOrEqual(20_000);
|
|
460
|
+
expect(result).toContain("Repeat with characterOffset=");
|
|
461
461
|
expect(result).not.toContain("NEXT_CHUNK_KEY");
|
|
462
|
+
|
|
463
|
+
const pages = [result];
|
|
464
|
+
let offsetMatch = result.match(/characterOffset=(\d+)/);
|
|
465
|
+
while (offsetMatch) {
|
|
466
|
+
const page = functions.expandTokens(
|
|
467
|
+
firstKey,
|
|
468
|
+
undefined,
|
|
469
|
+
undefined,
|
|
470
|
+
Number(offsetMatch[1])
|
|
471
|
+
);
|
|
472
|
+
pages.push(page);
|
|
473
|
+
offsetMatch = page.match(/Repeat with characterOffset=(\d+)/);
|
|
474
|
+
}
|
|
475
|
+
expect(pages.reduce((sum, page) => sum + (page.match(/^a+/)?.[0].length || 0), 0)).toBe(content.length);
|
|
462
476
|
});
|
|
463
477
|
|
|
464
478
|
it("should support a ranged read with real line numbers", () => {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
executeJqQuery,
|
|
3
|
+
jqToolResponseDefinition,
|
|
4
|
+
} from "../../../src/processors/tools/jqToolResponse";
|
|
5
|
+
import {
|
|
6
|
+
executeTail,
|
|
7
|
+
tailToolResponseDefinition,
|
|
8
|
+
} from "../../../src/processors/tools/tailToolResponse";
|
|
9
|
+
import {
|
|
10
|
+
executeListStoredToolResponses,
|
|
11
|
+
listStoredToolResponsesDefinition,
|
|
12
|
+
} from "../../../src/processors/tools/listStoredToolResponses";
|
|
13
|
+
|
|
14
|
+
describe("bounded retrieval tools", () => {
|
|
15
|
+
it("bounds and paginates JQ output", async () => {
|
|
16
|
+
const data = JSON.stringify(Array.from({ length: 100 }, (_, i) => `value-${i}-${"x".repeat(30)}`));
|
|
17
|
+
const first = await executeJqQuery(data, "jq-id", ".", ["jq-id"], {}, { maxCharacters: 1000 });
|
|
18
|
+
expect(first.length).toBeLessThanOrEqual(1000);
|
|
19
|
+
expect(first).toContain("Repeat with characterOffset=");
|
|
20
|
+
|
|
21
|
+
const offset = Number(first.match(/characterOffset=(\d+)/)?.[1]);
|
|
22
|
+
const next = await executeJqQuery(data, "jq-id", ".", ["jq-id"], {}, {
|
|
23
|
+
characterOffset: offset,
|
|
24
|
+
maxCharacters: 1000,
|
|
25
|
+
});
|
|
26
|
+
expect(next).not.toBe(first);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("bounds tail line lengths and supports backward pagination", async () => {
|
|
30
|
+
const data = ["first", `second ${"a".repeat(5000)}`, "third"].join("\n");
|
|
31
|
+
const result = await executeTail(data, "tail-id", ["tail-id"], {
|
|
32
|
+
lines: 1,
|
|
33
|
+
endLine: 2,
|
|
34
|
+
maxLineCharacters: 1000,
|
|
35
|
+
maxCharacters: 2000,
|
|
36
|
+
});
|
|
37
|
+
expect(result.length).toBeLessThanOrEqual(2000);
|
|
38
|
+
expect(result).toContain("Earlier lines available");
|
|
39
|
+
expect(result).toContain("lineCharacterOffset=1000");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("paginates stored response results", async () => {
|
|
43
|
+
const storage = { a: "one", b: "two", c: "three" };
|
|
44
|
+
const metadata = {
|
|
45
|
+
a: { toolCallId: "a", originalLength: 3, storedAt: 1 },
|
|
46
|
+
b: { toolCallId: "b", originalLength: 3, storedAt: 2 },
|
|
47
|
+
c: { toolCallId: "c", originalLength: 5, storedAt: 3 },
|
|
48
|
+
};
|
|
49
|
+
const result = await executeListStoredToolResponses(storage, metadata, {}, {
|
|
50
|
+
maxResults: 1,
|
|
51
|
+
resultOffset: 1,
|
|
52
|
+
});
|
|
53
|
+
expect(result).toContain("Tool Call ID: b");
|
|
54
|
+
expect(result).not.toContain("Tool Call ID: c");
|
|
55
|
+
expect(result).toContain("resultOffset=2");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("exposes pagination controls in each tool schema", () => {
|
|
59
|
+
const jqOptions = (jqToolResponseDefinition.function.parameters.properties as any).options;
|
|
60
|
+
const tailOptions = (tailToolResponseDefinition.function.parameters.properties as any).options;
|
|
61
|
+
const listOptions = (listStoredToolResponsesDefinition.function.parameters.properties as any).options;
|
|
62
|
+
expect(jqOptions.properties.characterOffset).toBeDefined();
|
|
63
|
+
expect(tailOptions.properties.endLine).toBeDefined();
|
|
64
|
+
expect(tailOptions.properties.maxCharacters).toBeDefined();
|
|
65
|
+
expect(listOptions.properties.resultOffset).toBeDefined();
|
|
66
|
+
expect(listOptions.properties.maxCharacters).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
executeGrep,
|
|
3
|
+
grepToolResponseDefinition,
|
|
4
|
+
} from "../../../src/processors/tools/grepToolResponse";
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Verifies grepToolResponse operates on the decompressed/plain stored content
|
|
@@ -69,4 +72,88 @@ describe("executeGrep", () => {
|
|
|
69
72
|
|
|
70
73
|
expect(result).toContain("No matches found");
|
|
71
74
|
});
|
|
75
|
+
|
|
76
|
+
it("bounds the total response and advertises the next result page", async () => {
|
|
77
|
+
const manyMatches = Array.from(
|
|
78
|
+
{ length: 20 },
|
|
79
|
+
(_, index) => `match ${index} ${"x".repeat(200)}`
|
|
80
|
+
).join("\n");
|
|
81
|
+
|
|
82
|
+
const result = await executeGrep(
|
|
83
|
+
manyMatches,
|
|
84
|
+
toolCallId,
|
|
85
|
+
"match",
|
|
86
|
+
[toolCallId],
|
|
87
|
+
{ maxCharacters: 1000, maxResults: 20 }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(result.length).toBeLessThanOrEqual(1000);
|
|
91
|
+
expect(result).toContain("More matches available");
|
|
92
|
+
expect(result).toMatch(/resultOffset=\d+/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("keeps a large log grep under the default response limit", async () => {
|
|
96
|
+
const logMatches = Array.from(
|
|
97
|
+
{ length: 500 },
|
|
98
|
+
(_, index) => `${index}: ${JSON.stringify({ level: 30, message: "sandbox runner", detail: "x".repeat(500) })}`
|
|
99
|
+
).join("\n");
|
|
100
|
+
|
|
101
|
+
const result = await executeGrep(
|
|
102
|
+
logMatches,
|
|
103
|
+
toolCallId,
|
|
104
|
+
"runner|sandbox",
|
|
105
|
+
[toolCallId]
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(result.length).toBeLessThanOrEqual(20_000);
|
|
109
|
+
expect(result).toContain("More matches available");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("paginates matches using resultOffset", async () => {
|
|
113
|
+
const result = await executeGrep(
|
|
114
|
+
["match zero", "match one", "match two"].join("\n"),
|
|
115
|
+
toolCallId,
|
|
116
|
+
"match",
|
|
117
|
+
[toolCallId],
|
|
118
|
+
{ maxResults: 1, resultOffset: 1 }
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(result).not.toContain("match zero");
|
|
122
|
+
expect(result).toContain("match one");
|
|
123
|
+
expect(result).not.toContain("match two");
|
|
124
|
+
expect(result).toContain("resultOffset=2");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("slices very long matching lines and lets callers page within the line", async () => {
|
|
128
|
+
const longLine = `prefix-${"a".repeat(5000)}-suffix`;
|
|
129
|
+
const firstPage = await executeGrep(
|
|
130
|
+
longLine,
|
|
131
|
+
toolCallId,
|
|
132
|
+
"prefix",
|
|
133
|
+
[toolCallId]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(firstPage.length).toBeLessThan(10_000);
|
|
137
|
+
expect(firstPage).toContain("line 1 truncated");
|
|
138
|
+
expect(firstPage).toContain("lineCharacterOffset=4000");
|
|
139
|
+
expect(firstPage).not.toContain("-suffix");
|
|
140
|
+
|
|
141
|
+
const secondPage = await executeGrep(
|
|
142
|
+
longLine,
|
|
143
|
+
toolCallId,
|
|
144
|
+
"prefix",
|
|
145
|
+
[toolCallId],
|
|
146
|
+
{ lineCharacterOffset: 4000 }
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(secondPage).toContain("-suffix");
|
|
150
|
+
expect(secondPage).toContain("End of matches");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("exposes pagination controls in the tool schema", () => {
|
|
154
|
+
const options = (grepToolResponseDefinition.function.parameters.properties as any).options;
|
|
155
|
+
expect(options.properties.resultOffset).toBeDefined();
|
|
156
|
+
expect(options.properties.lineCharacterOffset).toBeDefined();
|
|
157
|
+
expect(options.properties.maxCharacters).toBeDefined();
|
|
158
|
+
});
|
|
72
159
|
});
|