@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.
Files changed (153) hide show
  1. package/.generated/req.txt +1 -0
  2. package/.turbo/turbo-build.log +21 -0
  3. package/.turbo/turbo-test$colon$coverage.log +109 -0
  4. package/.turbo/turbo-test.log +141 -0
  5. package/.turbo/turbo-typecheck.log +4 -0
  6. package/CHANGELOG.md +16 -0
  7. package/ISSUES.md +540 -0
  8. package/LICENSE +21 -0
  9. package/README.md +128 -0
  10. package/banner.svg +97 -0
  11. package/coverage/lcov-report/base.css +224 -0
  12. package/coverage/lcov-report/block-navigation.js +87 -0
  13. package/coverage/lcov-report/core/agents/base/agent.ts.html +1705 -0
  14. package/coverage/lcov-report/core/agents/base/index.html +146 -0
  15. package/coverage/lcov-report/core/agents/base/output.ts.html +256 -0
  16. package/coverage/lcov-report/core/agents/base/utils.ts.html +694 -0
  17. package/coverage/lcov-report/core/agents/flow/engine.ts.html +928 -0
  18. package/coverage/lcov-report/core/agents/flow/flow-agent.ts.html +1462 -0
  19. package/coverage/lcov-report/core/agents/flow/index.html +146 -0
  20. package/coverage/lcov-report/core/agents/flow/messages.ts.html +508 -0
  21. package/coverage/lcov-report/core/agents/flow/steps/factory.ts.html +1975 -0
  22. package/coverage/lcov-report/core/agents/flow/steps/index.html +116 -0
  23. package/coverage/lcov-report/core/index.html +131 -0
  24. package/coverage/lcov-report/core/logger.ts.html +541 -0
  25. package/coverage/lcov-report/core/models/providers/index.html +116 -0
  26. package/coverage/lcov-report/core/models/providers/openai.ts.html +337 -0
  27. package/coverage/lcov-report/core/provider/index.html +131 -0
  28. package/coverage/lcov-report/core/provider/provider.ts.html +346 -0
  29. package/coverage/lcov-report/core/provider/usage.ts.html +376 -0
  30. package/coverage/lcov-report/core/tool.ts.html +577 -0
  31. package/coverage/lcov-report/favicon.png +0 -0
  32. package/coverage/lcov-report/index.html +221 -0
  33. package/coverage/lcov-report/lib/hooks.ts.html +262 -0
  34. package/coverage/lcov-report/lib/index.html +161 -0
  35. package/coverage/lcov-report/lib/middleware.ts.html +274 -0
  36. package/coverage/lcov-report/lib/runnable.ts.html +151 -0
  37. package/coverage/lcov-report/lib/trace.ts.html +520 -0
  38. package/coverage/lcov-report/prettify.css +1 -0
  39. package/coverage/lcov-report/prettify.js +2 -0
  40. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  41. package/coverage/lcov-report/sorter.js +210 -0
  42. package/coverage/lcov-report/utils/attempt.ts.html +199 -0
  43. package/coverage/lcov-report/utils/error.ts.html +421 -0
  44. package/coverage/lcov-report/utils/index.html +176 -0
  45. package/coverage/lcov-report/utils/resolve.ts.html +208 -0
  46. package/coverage/lcov-report/utils/result.ts.html +538 -0
  47. package/coverage/lcov-report/utils/zod.ts.html +178 -0
  48. package/coverage/lcov.info +1566 -0
  49. package/dist/index.d.mts +2883 -0
  50. package/dist/index.d.mts.map +1 -0
  51. package/dist/index.mjs +2312 -0
  52. package/dist/index.mjs.map +1 -0
  53. package/docs/core/agent.md +231 -0
  54. package/docs/core/hooks.md +95 -0
  55. package/docs/core/overview.md +87 -0
  56. package/docs/core/step.md +279 -0
  57. package/docs/core/tools.md +98 -0
  58. package/docs/core/workflow.md +235 -0
  59. package/docs/guides/create-agent.md +224 -0
  60. package/docs/guides/create-tool.md +137 -0
  61. package/docs/guides/create-workflow.md +374 -0
  62. package/docs/overview.md +244 -0
  63. package/docs/provider/models.md +55 -0
  64. package/docs/provider/overview.md +106 -0
  65. package/docs/provider/usage.md +100 -0
  66. package/docs/research/experimental-context.md +167 -0
  67. package/docs/research/gap-analysis.md +86 -0
  68. package/docs/research/prepare-step-and-active-tools.md +138 -0
  69. package/docs/research/sub-agent-model.md +249 -0
  70. package/docs/troubleshooting.md +60 -0
  71. package/logo.svg +17 -0
  72. package/models.config.json +18 -0
  73. package/package.json +60 -0
  74. package/scripts/generate-models.ts +324 -0
  75. package/src/core/agents/base/agent.test.ts +1522 -0
  76. package/src/core/agents/base/agent.ts +547 -0
  77. package/src/core/agents/base/output.test.ts +93 -0
  78. package/src/core/agents/base/output.ts +57 -0
  79. package/src/core/agents/base/types.test-d.ts +69 -0
  80. package/src/core/agents/base/types.ts +503 -0
  81. package/src/core/agents/base/utils.test.ts +397 -0
  82. package/src/core/agents/base/utils.ts +197 -0
  83. package/src/core/agents/flow/engine.test.ts +452 -0
  84. package/src/core/agents/flow/engine.ts +281 -0
  85. package/src/core/agents/flow/flow-agent.test.ts +1027 -0
  86. package/src/core/agents/flow/flow-agent.ts +473 -0
  87. package/src/core/agents/flow/messages.test.ts +198 -0
  88. package/src/core/agents/flow/messages.ts +141 -0
  89. package/src/core/agents/flow/steps/agent.test.ts +280 -0
  90. package/src/core/agents/flow/steps/agent.ts +87 -0
  91. package/src/core/agents/flow/steps/all.test.ts +300 -0
  92. package/src/core/agents/flow/steps/all.ts +73 -0
  93. package/src/core/agents/flow/steps/builder.ts +124 -0
  94. package/src/core/agents/flow/steps/each.test.ts +257 -0
  95. package/src/core/agents/flow/steps/each.ts +61 -0
  96. package/src/core/agents/flow/steps/factory.test-d.ts +50 -0
  97. package/src/core/agents/flow/steps/factory.test.ts +1025 -0
  98. package/src/core/agents/flow/steps/factory.ts +645 -0
  99. package/src/core/agents/flow/steps/map.test.ts +273 -0
  100. package/src/core/agents/flow/steps/map.ts +75 -0
  101. package/src/core/agents/flow/steps/race.test.ts +290 -0
  102. package/src/core/agents/flow/steps/race.ts +59 -0
  103. package/src/core/agents/flow/steps/reduce.test.ts +310 -0
  104. package/src/core/agents/flow/steps/reduce.ts +73 -0
  105. package/src/core/agents/flow/steps/result.ts +27 -0
  106. package/src/core/agents/flow/steps/step.test.ts +402 -0
  107. package/src/core/agents/flow/steps/step.ts +51 -0
  108. package/src/core/agents/flow/steps/while.test.ts +283 -0
  109. package/src/core/agents/flow/steps/while.ts +75 -0
  110. package/src/core/agents/flow/types.ts +348 -0
  111. package/src/core/logger.test.ts +163 -0
  112. package/src/core/logger.ts +152 -0
  113. package/src/core/models/index.test.ts +137 -0
  114. package/src/core/models/index.ts +152 -0
  115. package/src/core/models/providers/openai.ts +84 -0
  116. package/src/core/provider/provider.test.ts +128 -0
  117. package/src/core/provider/provider.ts +99 -0
  118. package/src/core/provider/types.ts +98 -0
  119. package/src/core/provider/usage.test.ts +304 -0
  120. package/src/core/provider/usage.ts +97 -0
  121. package/src/core/tool.test.ts +65 -0
  122. package/src/core/tool.ts +164 -0
  123. package/src/core/types.ts +66 -0
  124. package/src/index.ts +95 -0
  125. package/src/lib/context.test.ts +86 -0
  126. package/src/lib/context.ts +49 -0
  127. package/src/lib/hooks.test.ts +102 -0
  128. package/src/lib/hooks.ts +59 -0
  129. package/src/lib/middleware.test.ts +122 -0
  130. package/src/lib/middleware.ts +63 -0
  131. package/src/lib/runnable.test.ts +41 -0
  132. package/src/lib/runnable.ts +22 -0
  133. package/src/lib/trace.test.ts +291 -0
  134. package/src/lib/trace.ts +145 -0
  135. package/src/models/index.ts +123 -0
  136. package/src/models/providers/index.ts +15 -0
  137. package/src/models/providers/openai.ts +84 -0
  138. package/src/testing/context.ts +32 -0
  139. package/src/testing/index.ts +2 -0
  140. package/src/testing/logger.ts +19 -0
  141. package/src/utils/attempt.test.ts +127 -0
  142. package/src/utils/attempt.ts +38 -0
  143. package/src/utils/error.test.ts +179 -0
  144. package/src/utils/error.ts +112 -0
  145. package/src/utils/resolve.test.ts +38 -0
  146. package/src/utils/resolve.ts +41 -0
  147. package/src/utils/result.test.ts +79 -0
  148. package/src/utils/result.ts +151 -0
  149. package/src/utils/zod.test.ts +69 -0
  150. package/src/utils/zod.ts +31 -0
  151. package/tsconfig.json +25 -0
  152. package/tsdown.config.ts +15 -0
  153. 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;