@funkai/agents 0.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.
- package/.generated/req.txt +1 -0
- package/.turbo/turbo-build.log +21 -0
- package/.turbo/turbo-test$colon$coverage.log +109 -0
- package/.turbo/turbo-test.log +141 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +16 -0
- package/ISSUES.md +540 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/banner.svg +97 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/core/agents/base/agent.ts.html +1705 -0
- package/coverage/lcov-report/core/agents/base/index.html +146 -0
- package/coverage/lcov-report/core/agents/base/output.ts.html +256 -0
- package/coverage/lcov-report/core/agents/base/utils.ts.html +694 -0
- package/coverage/lcov-report/core/agents/flow/engine.ts.html +928 -0
- package/coverage/lcov-report/core/agents/flow/flow-agent.ts.html +1462 -0
- package/coverage/lcov-report/core/agents/flow/index.html +146 -0
- package/coverage/lcov-report/core/agents/flow/messages.ts.html +508 -0
- package/coverage/lcov-report/core/agents/flow/steps/factory.ts.html +1975 -0
- package/coverage/lcov-report/core/agents/flow/steps/index.html +116 -0
- package/coverage/lcov-report/core/index.html +131 -0
- package/coverage/lcov-report/core/logger.ts.html +541 -0
- package/coverage/lcov-report/core/models/providers/index.html +116 -0
- package/coverage/lcov-report/core/models/providers/openai.ts.html +337 -0
- package/coverage/lcov-report/core/provider/index.html +131 -0
- package/coverage/lcov-report/core/provider/provider.ts.html +346 -0
- package/coverage/lcov-report/core/provider/usage.ts.html +376 -0
- package/coverage/lcov-report/core/tool.ts.html +577 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +221 -0
- package/coverage/lcov-report/lib/hooks.ts.html +262 -0
- package/coverage/lcov-report/lib/index.html +161 -0
- package/coverage/lcov-report/lib/middleware.ts.html +274 -0
- package/coverage/lcov-report/lib/runnable.ts.html +151 -0
- package/coverage/lcov-report/lib/trace.ts.html +520 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/utils/attempt.ts.html +199 -0
- package/coverage/lcov-report/utils/error.ts.html +421 -0
- package/coverage/lcov-report/utils/index.html +176 -0
- package/coverage/lcov-report/utils/resolve.ts.html +208 -0
- package/coverage/lcov-report/utils/result.ts.html +538 -0
- package/coverage/lcov-report/utils/zod.ts.html +178 -0
- package/coverage/lcov.info +1566 -0
- package/dist/index.d.mts +2883 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2312 -0
- package/dist/index.mjs.map +1 -0
- package/docs/core/agent.md +231 -0
- package/docs/core/hooks.md +95 -0
- package/docs/core/overview.md +87 -0
- package/docs/core/step.md +279 -0
- package/docs/core/tools.md +98 -0
- package/docs/core/workflow.md +235 -0
- package/docs/guides/create-agent.md +224 -0
- package/docs/guides/create-tool.md +137 -0
- package/docs/guides/create-workflow.md +374 -0
- package/docs/overview.md +244 -0
- package/docs/provider/models.md +55 -0
- package/docs/provider/overview.md +106 -0
- package/docs/provider/usage.md +100 -0
- package/docs/research/experimental-context.md +167 -0
- package/docs/research/gap-analysis.md +86 -0
- package/docs/research/prepare-step-and-active-tools.md +138 -0
- package/docs/research/sub-agent-model.md +249 -0
- package/docs/troubleshooting.md +60 -0
- package/logo.svg +17 -0
- package/models.config.json +18 -0
- package/package.json +60 -0
- package/scripts/generate-models.ts +324 -0
- package/src/core/agents/base/agent.test.ts +1522 -0
- package/src/core/agents/base/agent.ts +547 -0
- package/src/core/agents/base/output.test.ts +93 -0
- package/src/core/agents/base/output.ts +57 -0
- package/src/core/agents/base/types.test-d.ts +69 -0
- package/src/core/agents/base/types.ts +503 -0
- package/src/core/agents/base/utils.test.ts +397 -0
- package/src/core/agents/base/utils.ts +197 -0
- package/src/core/agents/flow/engine.test.ts +452 -0
- package/src/core/agents/flow/engine.ts +281 -0
- package/src/core/agents/flow/flow-agent.test.ts +1027 -0
- package/src/core/agents/flow/flow-agent.ts +473 -0
- package/src/core/agents/flow/messages.test.ts +198 -0
- package/src/core/agents/flow/messages.ts +141 -0
- package/src/core/agents/flow/steps/agent.test.ts +280 -0
- package/src/core/agents/flow/steps/agent.ts +87 -0
- package/src/core/agents/flow/steps/all.test.ts +300 -0
- package/src/core/agents/flow/steps/all.ts +73 -0
- package/src/core/agents/flow/steps/builder.ts +124 -0
- package/src/core/agents/flow/steps/each.test.ts +257 -0
- package/src/core/agents/flow/steps/each.ts +61 -0
- package/src/core/agents/flow/steps/factory.test-d.ts +50 -0
- package/src/core/agents/flow/steps/factory.test.ts +1025 -0
- package/src/core/agents/flow/steps/factory.ts +645 -0
- package/src/core/agents/flow/steps/map.test.ts +273 -0
- package/src/core/agents/flow/steps/map.ts +75 -0
- package/src/core/agents/flow/steps/race.test.ts +290 -0
- package/src/core/agents/flow/steps/race.ts +59 -0
- package/src/core/agents/flow/steps/reduce.test.ts +310 -0
- package/src/core/agents/flow/steps/reduce.ts +73 -0
- package/src/core/agents/flow/steps/result.ts +27 -0
- package/src/core/agents/flow/steps/step.test.ts +402 -0
- package/src/core/agents/flow/steps/step.ts +51 -0
- package/src/core/agents/flow/steps/while.test.ts +283 -0
- package/src/core/agents/flow/steps/while.ts +75 -0
- package/src/core/agents/flow/types.ts +348 -0
- package/src/core/logger.test.ts +163 -0
- package/src/core/logger.ts +152 -0
- package/src/core/models/index.test.ts +137 -0
- package/src/core/models/index.ts +152 -0
- package/src/core/models/providers/openai.ts +84 -0
- package/src/core/provider/provider.test.ts +128 -0
- package/src/core/provider/provider.ts +99 -0
- package/src/core/provider/types.ts +98 -0
- package/src/core/provider/usage.test.ts +304 -0
- package/src/core/provider/usage.ts +97 -0
- package/src/core/tool.test.ts +65 -0
- package/src/core/tool.ts +164 -0
- package/src/core/types.ts +66 -0
- package/src/index.ts +95 -0
- package/src/lib/context.test.ts +86 -0
- package/src/lib/context.ts +49 -0
- package/src/lib/hooks.test.ts +102 -0
- package/src/lib/hooks.ts +59 -0
- package/src/lib/middleware.test.ts +122 -0
- package/src/lib/middleware.ts +63 -0
- package/src/lib/runnable.test.ts +41 -0
- package/src/lib/runnable.ts +22 -0
- package/src/lib/trace.test.ts +291 -0
- package/src/lib/trace.ts +145 -0
- package/src/models/index.ts +123 -0
- package/src/models/providers/index.ts +15 -0
- package/src/models/providers/openai.ts +84 -0
- package/src/testing/context.ts +32 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/logger.ts +19 -0
- package/src/utils/attempt.test.ts +127 -0
- package/src/utils/attempt.ts +38 -0
- package/src/utils/error.test.ts +179 -0
- package/src/utils/error.ts +112 -0
- package/src/utils/resolve.test.ts +38 -0
- package/src/utils/resolve.ts +41 -0
- package/src/utils/result.test.ts +79 -0
- package/src/utils/result.ts +151 -0
- package/src/utils/zod.test.ts +69 -0
- package/src/utils/zod.ts +31 -0
- package/tsconfig.json +25 -0
- package/tsdown.config.ts +15 -0
- package/vitest.config.ts +46 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createDefaultLogger } from "@/core/logger.js";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// createDefaultLogger
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
describe("createDefaultLogger", () => {
|
|
10
|
+
it("returns a logger with all required methods", () => {
|
|
11
|
+
const log = createDefaultLogger();
|
|
12
|
+
expect(log.debug).toBeDefined();
|
|
13
|
+
expect(log.info).toBeDefined();
|
|
14
|
+
expect(log.warn).toBeDefined();
|
|
15
|
+
expect(log.error).toBeDefined();
|
|
16
|
+
expect(log.child).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("logs info with message-first signature", () => {
|
|
20
|
+
const spy = vi.spyOn(console, "info").mockImplementation(() => {});
|
|
21
|
+
const log = createDefaultLogger();
|
|
22
|
+
|
|
23
|
+
log.info("hello world");
|
|
24
|
+
|
|
25
|
+
expect(spy).toHaveBeenCalledWith({}, "hello world");
|
|
26
|
+
spy.mockRestore();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("logs info with message and metadata", () => {
|
|
30
|
+
const spy = vi.spyOn(console, "info").mockImplementation(() => {});
|
|
31
|
+
const log = createDefaultLogger();
|
|
32
|
+
|
|
33
|
+
log.info("request completed", { status: 200 });
|
|
34
|
+
|
|
35
|
+
expect(spy).toHaveBeenCalledWith({ status: 200 }, "request completed");
|
|
36
|
+
spy.mockRestore();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("logs info with object-first (pino) signature", () => {
|
|
40
|
+
const spy = vi.spyOn(console, "info").mockImplementation(() => {});
|
|
41
|
+
const log = createDefaultLogger();
|
|
42
|
+
|
|
43
|
+
log.info({ requestId: "abc" }, "started");
|
|
44
|
+
|
|
45
|
+
expect(spy).toHaveBeenCalledWith({ requestId: "abc" }, "started");
|
|
46
|
+
spy.mockRestore();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("logs debug messages to console.debug", () => {
|
|
50
|
+
const spy = vi.spyOn(console, "debug").mockImplementation(() => {});
|
|
51
|
+
const log = createDefaultLogger();
|
|
52
|
+
|
|
53
|
+
log.debug("trace data");
|
|
54
|
+
|
|
55
|
+
expect(spy).toHaveBeenCalledWith({}, "trace data");
|
|
56
|
+
spy.mockRestore();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("logs warn messages to console.warn", () => {
|
|
60
|
+
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
61
|
+
const log = createDefaultLogger();
|
|
62
|
+
|
|
63
|
+
log.warn("deprecation notice");
|
|
64
|
+
|
|
65
|
+
expect(spy).toHaveBeenCalledWith({}, "deprecation notice");
|
|
66
|
+
spy.mockRestore();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("logs error messages to console.error", () => {
|
|
70
|
+
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
71
|
+
const log = createDefaultLogger();
|
|
72
|
+
|
|
73
|
+
log.error("fatal", { code: 500 });
|
|
74
|
+
|
|
75
|
+
expect(spy).toHaveBeenCalledWith({ code: 500 }, "fatal");
|
|
76
|
+
spy.mockRestore();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("creates initial bindings that appear in all log output", () => {
|
|
80
|
+
const spy = vi.spyOn(console, "info").mockImplementation(() => {});
|
|
81
|
+
const log = createDefaultLogger({ service: "api" });
|
|
82
|
+
|
|
83
|
+
log.info("boot");
|
|
84
|
+
|
|
85
|
+
expect(spy).toHaveBeenCalledWith({ service: "api" }, "boot");
|
|
86
|
+
spy.mockRestore();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("merges initial bindings with per-call metadata", () => {
|
|
90
|
+
const spy = vi.spyOn(console, "info").mockImplementation(() => {});
|
|
91
|
+
const log = createDefaultLogger({ service: "api" });
|
|
92
|
+
|
|
93
|
+
log.info("req", { path: "/health" });
|
|
94
|
+
|
|
95
|
+
expect(spy).toHaveBeenCalledWith({ service: "api", path: "/health" }, "req");
|
|
96
|
+
spy.mockRestore();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// child loggers
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
describe("child loggers", () => {
|
|
105
|
+
it("creates a child logger that inherits parent bindings", () => {
|
|
106
|
+
const spy = vi.spyOn(console, "info").mockImplementation(() => {});
|
|
107
|
+
const parent = createDefaultLogger({ service: "api" });
|
|
108
|
+
const child = parent.child({ workflow: "deploy" });
|
|
109
|
+
|
|
110
|
+
child.info("step started");
|
|
111
|
+
|
|
112
|
+
expect(spy).toHaveBeenCalledWith({ service: "api", workflow: "deploy" }, "step started");
|
|
113
|
+
spy.mockRestore();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("does not affect parent when child adds bindings", () => {
|
|
117
|
+
const spy = vi.spyOn(console, "info").mockImplementation(() => {});
|
|
118
|
+
const parent = createDefaultLogger({ service: "api" });
|
|
119
|
+
parent.child({ workflow: "deploy" });
|
|
120
|
+
|
|
121
|
+
parent.info("parent log");
|
|
122
|
+
|
|
123
|
+
expect(spy).toHaveBeenCalledWith({ service: "api" }, "parent log");
|
|
124
|
+
spy.mockRestore();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("supports multiple levels of child nesting", () => {
|
|
128
|
+
const spy = vi.spyOn(console, "info").mockImplementation(() => {});
|
|
129
|
+
const root = createDefaultLogger({ app: "serenity" });
|
|
130
|
+
const mid = root.child({ workflow: "build" });
|
|
131
|
+
const leaf = mid.child({ step: "compile" });
|
|
132
|
+
|
|
133
|
+
leaf.info("compiling");
|
|
134
|
+
|
|
135
|
+
expect(spy).toHaveBeenCalledWith(
|
|
136
|
+
{ app: "serenity", workflow: "build", step: "compile" },
|
|
137
|
+
"compiling",
|
|
138
|
+
);
|
|
139
|
+
spy.mockRestore();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("child bindings override parent bindings with same key", () => {
|
|
143
|
+
const spy = vi.spyOn(console, "info").mockImplementation(() => {});
|
|
144
|
+
const parent = createDefaultLogger({ scope: "parent" });
|
|
145
|
+
const child = parent.child({ scope: "child" });
|
|
146
|
+
|
|
147
|
+
child.info("scoped");
|
|
148
|
+
|
|
149
|
+
expect(spy).toHaveBeenCalledWith({ scope: "child" }, "scoped");
|
|
150
|
+
spy.mockRestore();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("child logger has all level methods", () => {
|
|
154
|
+
const parent = createDefaultLogger();
|
|
155
|
+
const child = parent.child({ step: "test" });
|
|
156
|
+
|
|
157
|
+
expect(child.debug).toBeDefined();
|
|
158
|
+
expect(child.info).toBeDefined();
|
|
159
|
+
expect(child.warn).toBeDefined();
|
|
160
|
+
expect(child.error).toBeDefined();
|
|
161
|
+
expect(child.child).toBeDefined();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pino-compatible leveled logger with child logger support.
|
|
3
|
+
*
|
|
4
|
+
* Consumers inject a pino instance (or any compatible logger);
|
|
5
|
+
* the SDK defines only the interface. Each method supports both
|
|
6
|
+
* `(msg, meta?)` and `(meta, msg)` call signatures to match pino's API.
|
|
7
|
+
*
|
|
8
|
+
* Child loggers accumulate bindings from parents — the framework
|
|
9
|
+
* uses `child()` at each scope boundary (workflow, step, agent)
|
|
10
|
+
* so log output automatically includes execution context.
|
|
11
|
+
*/
|
|
12
|
+
export interface Logger {
|
|
13
|
+
/**
|
|
14
|
+
* Log a message at the DEBUG level.
|
|
15
|
+
*
|
|
16
|
+
* Use for verbose diagnostic output that is typically silenced
|
|
17
|
+
* in production (e.g. intermediate state, resolved config).
|
|
18
|
+
*
|
|
19
|
+
* @param msg - Human-readable log message.
|
|
20
|
+
* @param meta - Optional structured metadata merged into the log entry.
|
|
21
|
+
*/
|
|
22
|
+
debug(msg: string, meta?: Record<string, unknown>): void;
|
|
23
|
+
/**
|
|
24
|
+
* Log a message at the DEBUG level (pino object-first overload).
|
|
25
|
+
*
|
|
26
|
+
* @param meta - Structured metadata merged into the log entry.
|
|
27
|
+
* @param msg - Human-readable log message.
|
|
28
|
+
*/
|
|
29
|
+
debug(meta: Record<string, unknown>, msg: string): void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Log a message at the INFO level.
|
|
33
|
+
*
|
|
34
|
+
* Use for routine operational events worth recording — step
|
|
35
|
+
* transitions, successful completions, and notable state changes.
|
|
36
|
+
*
|
|
37
|
+
* @param msg - Human-readable log message.
|
|
38
|
+
* @param meta - Optional structured metadata merged into the log entry.
|
|
39
|
+
*/
|
|
40
|
+
info(msg: string, meta?: Record<string, unknown>): void;
|
|
41
|
+
/**
|
|
42
|
+
* Log a message at the INFO level (pino object-first overload).
|
|
43
|
+
*
|
|
44
|
+
* @param meta - Structured metadata merged into the log entry.
|
|
45
|
+
* @param msg - Human-readable log message.
|
|
46
|
+
*/
|
|
47
|
+
info(meta: Record<string, unknown>, msg: string): void;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Log a message at the WARN level.
|
|
51
|
+
*
|
|
52
|
+
* Use for recoverable problems that do not halt execution but
|
|
53
|
+
* may indicate degraded behavior (e.g. retries, fallback paths).
|
|
54
|
+
*
|
|
55
|
+
* @param msg - Human-readable log message.
|
|
56
|
+
* @param meta - Optional structured metadata merged into the log entry.
|
|
57
|
+
*/
|
|
58
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
59
|
+
/**
|
|
60
|
+
* Log a message at the WARN level (pino object-first overload).
|
|
61
|
+
*
|
|
62
|
+
* @param meta - Structured metadata merged into the log entry.
|
|
63
|
+
* @param msg - Human-readable log message.
|
|
64
|
+
*/
|
|
65
|
+
warn(meta: Record<string, unknown>, msg: string): void;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Log a message at the ERROR level.
|
|
69
|
+
*
|
|
70
|
+
* Use for failures that prevent an operation from completing
|
|
71
|
+
* successfully — unhandled exceptions, rejected promises, or
|
|
72
|
+
* invariant violations.
|
|
73
|
+
*
|
|
74
|
+
* @param msg - Human-readable log message.
|
|
75
|
+
* @param meta - Optional structured metadata merged into the log entry.
|
|
76
|
+
*/
|
|
77
|
+
error(msg: string, meta?: Record<string, unknown>): void;
|
|
78
|
+
/**
|
|
79
|
+
* Log a message at the ERROR level (pino object-first overload).
|
|
80
|
+
*
|
|
81
|
+
* @param meta - Structured metadata merged into the log entry.
|
|
82
|
+
* @param msg - Human-readable log message.
|
|
83
|
+
*/
|
|
84
|
+
error(meta: Record<string, unknown>, msg: string): void;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a child logger that inherits all parent bindings.
|
|
88
|
+
*
|
|
89
|
+
* The returned logger automatically includes the merged bindings
|
|
90
|
+
* in every log entry. The framework calls `child()` at each scope
|
|
91
|
+
* boundary (workflow, step, agent) so downstream logs carry full
|
|
92
|
+
* execution context without manual threading.
|
|
93
|
+
*
|
|
94
|
+
* @param bindings - Key-value pairs merged into every log entry
|
|
95
|
+
* produced by the child (and its descendants).
|
|
96
|
+
* @returns A new {@link Logger} with the accumulated bindings.
|
|
97
|
+
*/
|
|
98
|
+
child(bindings: Record<string, unknown>): Logger;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a minimal console-based logger satisfying the {@link Logger} interface.
|
|
103
|
+
*
|
|
104
|
+
* Supports `child()` by merging bindings into a prefix object.
|
|
105
|
+
* Each log call prepends the accumulated bindings to the output.
|
|
106
|
+
*
|
|
107
|
+
* Used as the default when no pino-compatible logger is injected.
|
|
108
|
+
*/
|
|
109
|
+
export function createDefaultLogger(bindings?: Record<string, unknown>): Logger {
|
|
110
|
+
const prefix = bindings ?? {};
|
|
111
|
+
return {
|
|
112
|
+
debug(first: string | Record<string, unknown>, second?: string | Record<string, unknown>) {
|
|
113
|
+
writeLog(prefix, "debug", first, second);
|
|
114
|
+
},
|
|
115
|
+
info(first: string | Record<string, unknown>, second?: string | Record<string, unknown>) {
|
|
116
|
+
writeLog(prefix, "info", first, second);
|
|
117
|
+
},
|
|
118
|
+
warn(first: string | Record<string, unknown>, second?: string | Record<string, unknown>) {
|
|
119
|
+
writeLog(prefix, "warn", first, second);
|
|
120
|
+
},
|
|
121
|
+
error(first: string | Record<string, unknown>, second?: string | Record<string, unknown>) {
|
|
122
|
+
writeLog(prefix, "error", first, second);
|
|
123
|
+
},
|
|
124
|
+
child(childBindings: Record<string, unknown>): Logger {
|
|
125
|
+
return createDefaultLogger({ ...prefix, ...childBindings });
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
*
|
|
132
|
+
* @param bindings
|
|
133
|
+
* @param level
|
|
134
|
+
* @param first
|
|
135
|
+
* @param second
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
function writeLog(
|
|
139
|
+
bindings: Record<string, unknown>,
|
|
140
|
+
level: "debug" | "info" | "warn" | "error",
|
|
141
|
+
first: string | Record<string, unknown>,
|
|
142
|
+
second?: string | Record<string, unknown>,
|
|
143
|
+
): void {
|
|
144
|
+
if (typeof first === "string") {
|
|
145
|
+
const meta = second as Record<string, unknown> | undefined;
|
|
146
|
+
// eslint-disable-next-line security/detect-object-injection -- Log level is a controlled string from the logger, not user input
|
|
147
|
+
console[level]({ ...bindings, ...meta }, first);
|
|
148
|
+
} else {
|
|
149
|
+
// eslint-disable-next-line security/detect-object-injection -- Log level is a controlled string from the logger, not user input
|
|
150
|
+
console[level]({ ...bindings, ...first }, second as string);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { model, models, MODELS, tryModel } from "@/core/models/index.js";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// MODELS constant
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
describe("MODELS", () => {
|
|
10
|
+
it("is a non-empty array", () => {
|
|
11
|
+
expect(MODELS.length).toBeGreaterThan(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("every entry has required fields", () => {
|
|
15
|
+
for (const m of MODELS) {
|
|
16
|
+
expect(typeof m.id).toBe("string");
|
|
17
|
+
expect(m.id.length).toBeGreaterThan(0);
|
|
18
|
+
expect(["chat", "coding", "reasoning"]).toContain(m.category);
|
|
19
|
+
expect(typeof m.pricing.prompt).toBe("number");
|
|
20
|
+
expect(typeof m.pricing.completion).toBe("number");
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("contains known model IDs", () => {
|
|
25
|
+
const ids = MODELS.map((m) => m.id);
|
|
26
|
+
expect(ids).toContain("openai/gpt-5.2-codex");
|
|
27
|
+
expect(ids).toContain("openai/o4-mini");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("has no duplicate IDs", () => {
|
|
31
|
+
const ids = MODELS.map((m) => m.id);
|
|
32
|
+
const unique = new Set(ids);
|
|
33
|
+
expect(unique.size).toBe(ids.length);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// model()
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
describe("model()", () => {
|
|
42
|
+
it("returns the model definition for a known ID", () => {
|
|
43
|
+
const result = model("openai/gpt-5.2-codex");
|
|
44
|
+
|
|
45
|
+
expect(result.id).toBe("openai/gpt-5.2-codex");
|
|
46
|
+
expect(result.category).toBe("coding");
|
|
47
|
+
expect(typeof result.pricing.prompt).toBe("number");
|
|
48
|
+
expect(typeof result.pricing.completion).toBe("number");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("throws for an unknown model ID", () => {
|
|
52
|
+
expect(() => model("nonexistent/model-99")).toThrow("Unknown model: nonexistent/model-99");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns correct category for reasoning models", () => {
|
|
56
|
+
const result = model("openai/o3");
|
|
57
|
+
|
|
58
|
+
expect(result.category).toBe("reasoning");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns correct category for chat models", () => {
|
|
62
|
+
const result = model("openai/gpt-5.2");
|
|
63
|
+
|
|
64
|
+
expect(result.category).toBe("chat");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// tryModel()
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
describe("tryModel()", () => {
|
|
73
|
+
it("returns the model definition for a known ID", () => {
|
|
74
|
+
const result = tryModel("openai/gpt-5.2-codex");
|
|
75
|
+
|
|
76
|
+
expect(result).toBeDefined();
|
|
77
|
+
if (!result) {
|
|
78
|
+
throw new Error("Expected result to be defined");
|
|
79
|
+
}
|
|
80
|
+
expect(result.id).toBe("openai/gpt-5.2-codex");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns undefined for an unknown model ID", () => {
|
|
84
|
+
const result = tryModel("nonexistent/model-99");
|
|
85
|
+
|
|
86
|
+
expect(result).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("does not throw for unknown IDs", () => {
|
|
90
|
+
expect(() => tryModel("nonexistent/model-99")).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// models()
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
describe("models()", () => {
|
|
99
|
+
it("returns all models when called without filter", () => {
|
|
100
|
+
const result = models();
|
|
101
|
+
|
|
102
|
+
expect(result.length).toBe(MODELS.length);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("filters models by category", () => {
|
|
106
|
+
const codingModels = models((m) => m.category === "coding");
|
|
107
|
+
|
|
108
|
+
expect(codingModels.length).toBeGreaterThan(0);
|
|
109
|
+
for (const m of codingModels) {
|
|
110
|
+
expect(m.category).toBe("coding");
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("returns reasoning models when filtered", () => {
|
|
115
|
+
const reasoningModels = models((m) => m.category === "reasoning");
|
|
116
|
+
|
|
117
|
+
expect(reasoningModels.length).toBeGreaterThan(0);
|
|
118
|
+
for (const m of reasoningModels) {
|
|
119
|
+
expect(m.category).toBe("reasoning");
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("returns empty array when filter matches nothing", () => {
|
|
124
|
+
const result = models(() => false);
|
|
125
|
+
|
|
126
|
+
expect(result).toEqual([]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("supports arbitrary filter predicates", () => {
|
|
130
|
+
const result = models((m) => m.pricing.prompt > 0.000001);
|
|
131
|
+
|
|
132
|
+
expect(result.length).toBeGreaterThan(0);
|
|
133
|
+
for (const m of result) {
|
|
134
|
+
expect(m.pricing.prompt).toBeGreaterThan(0.000001);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// ──────────────────────────────────────────────────────────────
|
|
2
|
+
// █████╗ ██████╗ ███████╗███╗ ██╗████████╗███████╗
|
|
3
|
+
// ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔════╝
|
|
4
|
+
// ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ███████╗
|
|
5
|
+
// ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║
|
|
6
|
+
// ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ███████║
|
|
7
|
+
// ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝
|
|
8
|
+
//
|
|
9
|
+
// AUTO-GENERATED — DO NOT EDIT
|
|
10
|
+
// Update: pnpm --filter=@pkg/agent-sdk generate:models
|
|
11
|
+
// ──────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
import { P, match } from "ts-pattern";
|
|
14
|
+
import type { LiteralUnion } from "type-fest";
|
|
15
|
+
|
|
16
|
+
import { OPENAI_MODELS } from "@/core/models/providers/openai.js";
|
|
17
|
+
|
|
18
|
+
const GENERATED_MODELS = [...OPENAI_MODELS] as const;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Supported OpenRouter model identifiers, derived from the generated {@link MODELS} array.
|
|
22
|
+
*/
|
|
23
|
+
export type OpenRouterLanguageModelId = (typeof GENERATED_MODELS)[number]["id"];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A model identifier that suggests known OpenRouter models but accepts any string.
|
|
27
|
+
*
|
|
28
|
+
* Provides autocomplete for cataloged models while allowing arbitrary
|
|
29
|
+
* model IDs for new or custom models not yet in the catalog.
|
|
30
|
+
*/
|
|
31
|
+
export type ModelId = LiteralUnion<OpenRouterLanguageModelId, string>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Model category for classification and filtering.
|
|
35
|
+
*/
|
|
36
|
+
export type ModelCategory = "chat" | "coding" | "reasoning";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Per-model pricing in USD per token.
|
|
40
|
+
*
|
|
41
|
+
* Field names match the OpenRouter API convention. All values are
|
|
42
|
+
* per-token (or per-unit) rates as numbers. Optional fields are
|
|
43
|
+
* omitted when the provider does not support them.
|
|
44
|
+
*/
|
|
45
|
+
export interface ModelPricing {
|
|
46
|
+
/** Cost per input (prompt) token. */
|
|
47
|
+
prompt: number;
|
|
48
|
+
|
|
49
|
+
/** Cost per output (completion) token. */
|
|
50
|
+
completion: number;
|
|
51
|
+
|
|
52
|
+
/** Cost per cached input token (read). */
|
|
53
|
+
inputCacheRead?: number;
|
|
54
|
+
|
|
55
|
+
/** Cost per cached input token (write). */
|
|
56
|
+
inputCacheWrite?: number;
|
|
57
|
+
|
|
58
|
+
/** Cost per web search request. */
|
|
59
|
+
webSearch?: number;
|
|
60
|
+
|
|
61
|
+
/** Cost per internal reasoning token. */
|
|
62
|
+
internalReasoning?: number;
|
|
63
|
+
|
|
64
|
+
/** Cost per image input token. */
|
|
65
|
+
image?: number;
|
|
66
|
+
|
|
67
|
+
/** Cost per audio input second. */
|
|
68
|
+
audio?: number;
|
|
69
|
+
|
|
70
|
+
/** Cost per audio output second. */
|
|
71
|
+
audioOutput?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Model definition with metadata and pricing.
|
|
76
|
+
*/
|
|
77
|
+
export interface ModelDefinition {
|
|
78
|
+
/** OpenRouter model identifier (e.g. `"openai/gpt-5.2-codex"`). */
|
|
79
|
+
id: string;
|
|
80
|
+
|
|
81
|
+
/** Model category for classification. */
|
|
82
|
+
category: ModelCategory;
|
|
83
|
+
|
|
84
|
+
/** Token pricing rates. */
|
|
85
|
+
pricing: ModelPricing;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Supported OpenRouter models with pricing data.
|
|
90
|
+
*/
|
|
91
|
+
export const MODELS = GENERATED_MODELS satisfies readonly ModelDefinition[];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Look up a model definition by its identifier.
|
|
95
|
+
*
|
|
96
|
+
* @param id - The model identifier to look up.
|
|
97
|
+
* @returns The matching model definition.
|
|
98
|
+
* @throws {Error} If no model matches the given ID.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const m = model('openai/gpt-5.2-codex')
|
|
103
|
+
* console.log(m.pricing.prompt) // 0.00000175
|
|
104
|
+
* console.log(m.category) // 'coding'
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function model(id: ModelId): ModelDefinition {
|
|
108
|
+
const found = MODELS.find((m) => m.id === id);
|
|
109
|
+
if (!found) {
|
|
110
|
+
throw new Error(`Unknown model: ${id}`);
|
|
111
|
+
}
|
|
112
|
+
return found;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Look up a model definition by its identifier, returning `undefined` if not found.
|
|
117
|
+
*
|
|
118
|
+
* Unlike {@link model}, this does not throw for unknown IDs — useful when
|
|
119
|
+
* the caller can gracefully handle missing pricing (e.g. non-OpenRouter models).
|
|
120
|
+
*
|
|
121
|
+
* @param id - The model identifier to look up.
|
|
122
|
+
* @returns The matching model definition, or `undefined`.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* const m = tryModel('anthropic/claude-sonnet-4-20250514')
|
|
127
|
+
* if (m) {
|
|
128
|
+
* console.log(m.pricing.prompt)
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function tryModel(id: ModelId): ModelDefinition | undefined {
|
|
133
|
+
return MODELS.find((m) => m.id === id);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Return supported model definitions, optionally filtered.
|
|
138
|
+
*
|
|
139
|
+
* @param filter - Optional predicate to filter models.
|
|
140
|
+
* @returns A readonly array of matching model definitions.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const all = models()
|
|
145
|
+
* const reasoning = models((m) => m.category === 'reasoning')
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export function models(filter?: (m: ModelDefinition) => boolean): readonly ModelDefinition[] {
|
|
149
|
+
return match(filter)
|
|
150
|
+
.with(P.nullish, () => MODELS)
|
|
151
|
+
.otherwise((fn) => MODELS.filter(fn));
|
|
152
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// ──────────────────────────────────────────────────────────────
|
|
2
|
+
// █████╗ ██████╗ ███████╗███╗ ██╗████████╗███████╗
|
|
3
|
+
// ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔════╝
|
|
4
|
+
// ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ███████╗
|
|
5
|
+
// ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║
|
|
6
|
+
// ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ███████║
|
|
7
|
+
// ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝
|
|
8
|
+
//
|
|
9
|
+
// AUTO-GENERATED — DO NOT EDIT
|
|
10
|
+
// Update: pnpm --filter=@pkg/agent-sdk generate:models
|
|
11
|
+
// ──────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export const OPENAI_MODELS = [
|
|
14
|
+
{
|
|
15
|
+
id: "openai/gpt-5.2-codex",
|
|
16
|
+
category: "coding",
|
|
17
|
+
pricing: { prompt: 0.00000175, completion: 0.000014, inputCacheRead: 1.75e-7, webSearch: 0.01 },
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "openai/gpt-5.2",
|
|
21
|
+
category: "chat",
|
|
22
|
+
pricing: { prompt: 0.00000175, completion: 0.000014, inputCacheRead: 1.75e-7, webSearch: 0.01 },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "openai/gpt-5.1",
|
|
26
|
+
category: "chat",
|
|
27
|
+
pricing: { prompt: 0.00000125, completion: 0.00001, inputCacheRead: 1.25e-7, webSearch: 0.01 },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "openai/gpt-5",
|
|
31
|
+
category: "chat",
|
|
32
|
+
pricing: { prompt: 0.00000125, completion: 0.00001, inputCacheRead: 1.25e-7, webSearch: 0.01 },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "openai/gpt-5-mini",
|
|
36
|
+
category: "chat",
|
|
37
|
+
pricing: { prompt: 2.5e-7, completion: 0.000002, inputCacheRead: 2.5e-8, webSearch: 0.01 },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "openai/gpt-5-nano",
|
|
41
|
+
category: "chat",
|
|
42
|
+
pricing: { prompt: 5e-8, completion: 4e-7, inputCacheRead: 5e-9, webSearch: 0.01 },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "openai/gpt-4.1",
|
|
46
|
+
category: "chat",
|
|
47
|
+
pricing: { prompt: 0.000002, completion: 0.000008, inputCacheRead: 5e-7, webSearch: 0.01 },
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "openai/gpt-4.1-mini",
|
|
51
|
+
category: "chat",
|
|
52
|
+
pricing: { prompt: 4e-7, completion: 0.0000016, inputCacheRead: 1e-7, webSearch: 0.01 },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "openai/gpt-4.1-nano",
|
|
56
|
+
category: "chat",
|
|
57
|
+
pricing: { prompt: 1e-7, completion: 4e-7, inputCacheRead: 2.5e-8, webSearch: 0.01 },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "openai/gpt-4o",
|
|
61
|
+
category: "chat",
|
|
62
|
+
pricing: { prompt: 0.0000025, completion: 0.00001, inputCacheRead: 0.00000125 },
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "openai/gpt-4o-mini",
|
|
66
|
+
category: "chat",
|
|
67
|
+
pricing: { prompt: 1.5e-7, completion: 6e-7, inputCacheRead: 7.5e-8 },
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "openai/o3",
|
|
71
|
+
category: "reasoning",
|
|
72
|
+
pricing: { prompt: 0.000002, completion: 0.000008, inputCacheRead: 5e-7, webSearch: 0.01 },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "openai/o3-mini",
|
|
76
|
+
category: "reasoning",
|
|
77
|
+
pricing: { prompt: 0.0000011, completion: 0.0000044, inputCacheRead: 5.5e-7 },
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "openai/o4-mini",
|
|
81
|
+
category: "reasoning",
|
|
82
|
+
pricing: { prompt: 0.0000011, completion: 0.0000044, inputCacheRead: 2.75e-7, webSearch: 0.01 },
|
|
83
|
+
},
|
|
84
|
+
] as const;
|