@funkai/models 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 (213) hide show
  1. package/.generated/entries.json +23 -0
  2. package/.generated/req.txt +1 -0
  3. package/.turbo/turbo-build.log +145 -0
  4. package/.turbo/turbo-typecheck.log +4 -0
  5. package/CHANGELOG.md +23 -0
  6. package/README.md +95 -0
  7. package/dist/alibaba-B6q4Ng1R.mjs +957 -0
  8. package/dist/alibaba-B6q4Ng1R.mjs.map +1 -0
  9. package/dist/amazon-bedrock-Cv9AHQBH.mjs +2070 -0
  10. package/dist/amazon-bedrock-Cv9AHQBH.mjs.map +1 -0
  11. package/dist/anthropic-yB7ST97_.mjs +651 -0
  12. package/dist/anthropic-yB7ST97_.mjs.map +1 -0
  13. package/dist/cerebras-COfl7XM-.mjs +95 -0
  14. package/dist/cerebras-COfl7XM-.mjs.map +1 -0
  15. package/dist/cohere-B7TgO0hT.mjs +271 -0
  16. package/dist/cohere-B7TgO0hT.mjs.map +1 -0
  17. package/dist/deepinfra-B0GxUwCG.mjs +636 -0
  18. package/dist/deepinfra-B0GxUwCG.mjs.map +1 -0
  19. package/dist/deepseek-D64ZEsvS.mjs +50 -0
  20. package/dist/deepseek-D64ZEsvS.mjs.map +1 -0
  21. package/dist/fireworks-ai-DJYvdAi_.mjs +304 -0
  22. package/dist/fireworks-ai-DJYvdAi_.mjs.map +1 -0
  23. package/dist/google-BypRl349.mjs +833 -0
  24. package/dist/google-BypRl349.mjs.map +1 -0
  25. package/dist/google-vertex-DbS-zTGD.mjs +730 -0
  26. package/dist/google-vertex-DbS-zTGD.mjs.map +1 -0
  27. package/dist/groq-ei_PerYi.mjs +381 -0
  28. package/dist/groq-ei_PerYi.mjs.map +1 -0
  29. package/dist/huggingface-DaM1EeLP.mjs +456 -0
  30. package/dist/huggingface-DaM1EeLP.mjs.map +1 -0
  31. package/dist/inception-CspEzqNV.mjs +101 -0
  32. package/dist/inception-CspEzqNV.mjs.map +1 -0
  33. package/dist/index.d.mts +30314 -0
  34. package/dist/index.d.mts.map +1 -0
  35. package/dist/index.mjs +271 -0
  36. package/dist/index.mjs.map +1 -0
  37. package/dist/llama-Cf3-koap.mjs +161 -0
  38. package/dist/llama-Cf3-koap.mjs.map +1 -0
  39. package/dist/mistral-BI9MdAO4.mjs +579 -0
  40. package/dist/mistral-BI9MdAO4.mjs.map +1 -0
  41. package/dist/nvidia-COHacuoa.mjs +1625 -0
  42. package/dist/nvidia-COHacuoa.mjs.map +1 -0
  43. package/dist/openai-C0nCfZUq.mjs +1023 -0
  44. package/dist/openai-C0nCfZUq.mjs.map +1 -0
  45. package/dist/openrouter-DSFzxKQb.mjs +4608 -0
  46. package/dist/openrouter-DSFzxKQb.mjs.map +1 -0
  47. package/dist/perplexity-zeZ2WlBU.mjs +96 -0
  48. package/dist/perplexity-zeZ2WlBU.mjs.map +1 -0
  49. package/dist/providers/alibaba.d.mts +1795 -0
  50. package/dist/providers/alibaba.d.mts.map +1 -0
  51. package/dist/providers/alibaba.mjs +39 -0
  52. package/dist/providers/alibaba.mjs.map +1 -0
  53. package/dist/providers/amazon-bedrock.d.mts +3713 -0
  54. package/dist/providers/amazon-bedrock.d.mts.map +1 -0
  55. package/dist/providers/amazon-bedrock.mjs +39 -0
  56. package/dist/providers/amazon-bedrock.mjs.map +1 -0
  57. package/dist/providers/anthropic.d.mts +1109 -0
  58. package/dist/providers/anthropic.d.mts.map +1 -0
  59. package/dist/providers/anthropic.mjs +39 -0
  60. package/dist/providers/anthropic.mjs.map +1 -0
  61. package/dist/providers/cerebras.d.mts +219 -0
  62. package/dist/providers/cerebras.d.mts.map +1 -0
  63. package/dist/providers/cerebras.mjs +39 -0
  64. package/dist/providers/cerebras.mjs.map +1 -0
  65. package/dist/providers/cohere.d.mts +555 -0
  66. package/dist/providers/cohere.d.mts.map +1 -0
  67. package/dist/providers/cohere.mjs +39 -0
  68. package/dist/providers/cohere.mjs.map +1 -0
  69. package/dist/providers/deepinfra.d.mts +1245 -0
  70. package/dist/providers/deepinfra.d.mts.map +1 -0
  71. package/dist/providers/deepinfra.mjs +39 -0
  72. package/dist/providers/deepinfra.mjs.map +1 -0
  73. package/dist/providers/deepseek.d.mts +139 -0
  74. package/dist/providers/deepseek.d.mts.map +1 -0
  75. package/dist/providers/deepseek.mjs +39 -0
  76. package/dist/providers/deepseek.mjs.map +1 -0
  77. package/dist/providers/fireworks-ai.d.mts +611 -0
  78. package/dist/providers/fireworks-ai.d.mts.map +1 -0
  79. package/dist/providers/fireworks-ai.mjs +39 -0
  80. package/dist/providers/fireworks-ai.mjs.map +1 -0
  81. package/dist/providers/google-vertex.d.mts +1227 -0
  82. package/dist/providers/google-vertex.d.mts.map +1 -0
  83. package/dist/providers/google-vertex.mjs +39 -0
  84. package/dist/providers/google-vertex.mjs.map +1 -0
  85. package/dist/providers/google.d.mts +1359 -0
  86. package/dist/providers/google.d.mts.map +1 -0
  87. package/dist/providers/google.mjs +39 -0
  88. package/dist/providers/google.mjs.map +1 -0
  89. package/dist/providers/groq.d.mts +765 -0
  90. package/dist/providers/groq.d.mts.map +1 -0
  91. package/dist/providers/groq.mjs +39 -0
  92. package/dist/providers/groq.mjs.map +1 -0
  93. package/dist/providers/huggingface.d.mts +901 -0
  94. package/dist/providers/huggingface.d.mts.map +1 -0
  95. package/dist/providers/huggingface.mjs +39 -0
  96. package/dist/providers/huggingface.mjs.map +1 -0
  97. package/dist/providers/inception.d.mts +231 -0
  98. package/dist/providers/inception.d.mts.map +1 -0
  99. package/dist/providers/inception.mjs +39 -0
  100. package/dist/providers/inception.mjs.map +1 -0
  101. package/dist/providers/llama.d.mts +345 -0
  102. package/dist/providers/llama.d.mts.map +1 -0
  103. package/dist/providers/llama.mjs +39 -0
  104. package/dist/providers/llama.mjs.map +1 -0
  105. package/dist/providers/mistral.d.mts +1143 -0
  106. package/dist/providers/mistral.d.mts.map +1 -0
  107. package/dist/providers/mistral.mjs +39 -0
  108. package/dist/providers/mistral.mjs.map +1 -0
  109. package/dist/providers/nvidia.d.mts +3117 -0
  110. package/dist/providers/nvidia.d.mts.map +1 -0
  111. package/dist/providers/nvidia.mjs +39 -0
  112. package/dist/providers/nvidia.mjs.map +1 -0
  113. package/dist/providers/openai.d.mts +1963 -0
  114. package/dist/providers/openai.d.mts.map +1 -0
  115. package/dist/providers/openai.mjs +39 -0
  116. package/dist/providers/openai.mjs.map +1 -0
  117. package/dist/providers/openrouter.d.mts +8531 -0
  118. package/dist/providers/openrouter.d.mts.map +1 -0
  119. package/dist/providers/openrouter.mjs +39 -0
  120. package/dist/providers/openrouter.mjs.map +1 -0
  121. package/dist/providers/perplexity.d.mts +221 -0
  122. package/dist/providers/perplexity.d.mts.map +1 -0
  123. package/dist/providers/perplexity.mjs +39 -0
  124. package/dist/providers/perplexity.mjs.map +1 -0
  125. package/dist/providers/togetherai.d.mts +767 -0
  126. package/dist/providers/togetherai.d.mts.map +1 -0
  127. package/dist/providers/togetherai.mjs +39 -0
  128. package/dist/providers/togetherai.mjs.map +1 -0
  129. package/dist/providers/xai.d.mts +1161 -0
  130. package/dist/providers/xai.d.mts.map +1 -0
  131. package/dist/providers/xai.mjs +39 -0
  132. package/dist/providers/xai.mjs.map +1 -0
  133. package/dist/togetherai-BvcxUfPE.mjs +382 -0
  134. package/dist/togetherai-BvcxUfPE.mjs.map +1 -0
  135. package/dist/types-DjdaZckF.d.mts +71 -0
  136. package/dist/types-DjdaZckF.d.mts.map +1 -0
  137. package/dist/xai-fSuAkQJo.mjs +587 -0
  138. package/dist/xai-fSuAkQJo.mjs.map +1 -0
  139. package/docs/catalog/filtering.md +102 -0
  140. package/docs/catalog/overview.md +168 -0
  141. package/docs/catalog/providers.md +73 -0
  142. package/docs/cost/overview.md +125 -0
  143. package/docs/guides/filter-models.md +113 -0
  144. package/docs/guides/setup-resolver.md +106 -0
  145. package/docs/guides/track-costs.md +133 -0
  146. package/docs/overview.md +139 -0
  147. package/docs/provider/configuration.md +100 -0
  148. package/docs/provider/openrouter.md +105 -0
  149. package/docs/provider/overview.md +131 -0
  150. package/docs/troubleshooting.md +100 -0
  151. package/package.json +142 -0
  152. package/providers.json +39 -0
  153. package/scripts/generate-models.ts +392 -0
  154. package/src/catalog/index.test.ts +124 -0
  155. package/src/catalog/index.ts +65 -0
  156. package/src/catalog/providers/alibaba.ts +468 -0
  157. package/src/catalog/providers/amazon-bedrock.ts +941 -0
  158. package/src/catalog/providers/anthropic.ts +270 -0
  159. package/src/catalog/providers/cerebras.ts +61 -0
  160. package/src/catalog/providers/cohere.ts +149 -0
  161. package/src/catalog/providers/deepinfra.ts +325 -0
  162. package/src/catalog/providers/deepseek.ts +39 -0
  163. package/src/catalog/providers/fireworks-ai.ts +160 -0
  164. package/src/catalog/providers/google-vertex.ts +314 -0
  165. package/src/catalog/providers/google.ts +347 -0
  166. package/src/catalog/providers/groq.ts +204 -0
  167. package/src/catalog/providers/huggingface.ts +237 -0
  168. package/src/catalog/providers/inception.ts +61 -0
  169. package/src/catalog/providers/index.ts +59 -0
  170. package/src/catalog/providers/llama.ts +94 -0
  171. package/src/catalog/providers/mistral.ts +303 -0
  172. package/src/catalog/providers/nvidia.ts +820 -0
  173. package/src/catalog/providers/openai.ts +501 -0
  174. package/src/catalog/providers/openrouter.ts +2201 -0
  175. package/src/catalog/providers/perplexity.ts +61 -0
  176. package/src/catalog/providers/togetherai.ts +204 -0
  177. package/src/catalog/providers/xai.ts +292 -0
  178. package/src/catalog/types.ts +86 -0
  179. package/src/cost/calculate.test.ts +157 -0
  180. package/src/cost/calculate.ts +43 -0
  181. package/src/cost/index.ts +2 -0
  182. package/src/cost/types.ts +25 -0
  183. package/src/index.ts +25 -0
  184. package/src/provider/index.ts +9 -0
  185. package/src/provider/openrouter.test.ts +125 -0
  186. package/src/provider/openrouter.ts +110 -0
  187. package/src/provider/resolver.test.ts +138 -0
  188. package/src/provider/resolver.ts +125 -0
  189. package/src/provider/types.ts +39 -0
  190. package/src/providers/alibaba.ts +65 -0
  191. package/src/providers/amazon-bedrock.ts +67 -0
  192. package/src/providers/anthropic.ts +65 -0
  193. package/src/providers/cerebras.ts +65 -0
  194. package/src/providers/cohere.ts +65 -0
  195. package/src/providers/deepinfra.ts +65 -0
  196. package/src/providers/deepseek.ts +65 -0
  197. package/src/providers/fireworks-ai.ts +65 -0
  198. package/src/providers/google-vertex.ts +67 -0
  199. package/src/providers/google.ts +65 -0
  200. package/src/providers/groq.ts +65 -0
  201. package/src/providers/huggingface.ts +67 -0
  202. package/src/providers/inception.ts +65 -0
  203. package/src/providers/llama.ts +65 -0
  204. package/src/providers/mistral.ts +65 -0
  205. package/src/providers/nvidia.ts +65 -0
  206. package/src/providers/openai.ts +65 -0
  207. package/src/providers/openrouter.ts +67 -0
  208. package/src/providers/perplexity.ts +67 -0
  209. package/src/providers/togetherai.ts +65 -0
  210. package/src/providers/xai.ts +65 -0
  211. package/tsconfig.json +25 -0
  212. package/tsdown.config.ts +23 -0
  213. package/vitest.config.ts +29 -0
@@ -0,0 +1,157 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { calculateCost } from "./calculate.js";
4
+ import type { ModelPricing } from "@/catalog/types.js";
5
+ import type { TokenUsage } from "@/provider/types.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Helpers
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const ZERO_USAGE: TokenUsage = {
12
+ inputTokens: 0,
13
+ outputTokens: 0,
14
+ totalTokens: 0,
15
+ cacheReadTokens: 0,
16
+ cacheWriteTokens: 0,
17
+ reasoningTokens: 0,
18
+ };
19
+
20
+ const BASIC_PRICING: ModelPricing = {
21
+ input: 0.000002,
22
+ output: 0.000008,
23
+ };
24
+
25
+ const FULL_PRICING: ModelPricing = {
26
+ input: 0.000002,
27
+ output: 0.000008,
28
+ cacheRead: 0.0000005,
29
+ cacheWrite: 0.000001,
30
+ };
31
+
32
+ const REASONING_PRICING: ModelPricing = {
33
+ input: 0.000002,
34
+ output: 0.000008,
35
+ cacheRead: 0.0000005,
36
+ cacheWrite: 0.000001,
37
+ reasoning: 0.000012,
38
+ };
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // calculateCost()
42
+ // ---------------------------------------------------------------------------
43
+
44
+ describe("calculateCost()", () => {
45
+ it("returns all zeros for zero usage", () => {
46
+ const result = calculateCost(ZERO_USAGE, FULL_PRICING);
47
+
48
+ expect(result.input).toBe(0);
49
+ expect(result.output).toBe(0);
50
+ expect(result.cacheRead).toBe(0);
51
+ expect(result.cacheWrite).toBe(0);
52
+ expect(result.reasoning).toBe(0);
53
+ expect(result.total).toBe(0);
54
+ });
55
+
56
+ it("calculates input and output costs", () => {
57
+ const usage: TokenUsage = {
58
+ ...ZERO_USAGE,
59
+ inputTokens: 1000,
60
+ outputTokens: 500,
61
+ totalTokens: 1500,
62
+ };
63
+
64
+ const result = calculateCost(usage, BASIC_PRICING);
65
+
66
+ expect(result.input).toBeCloseTo(0.002);
67
+ expect(result.output).toBeCloseTo(0.004);
68
+ expect(result.total).toBeCloseTo(0.006);
69
+ });
70
+
71
+ it("handles pricing without optional fields", () => {
72
+ const usage: TokenUsage = {
73
+ inputTokens: 1000,
74
+ outputTokens: 500,
75
+ totalTokens: 1500,
76
+ cacheReadTokens: 200,
77
+ cacheWriteTokens: 100,
78
+ reasoningTokens: 50,
79
+ };
80
+
81
+ const result = calculateCost(usage, BASIC_PRICING);
82
+
83
+ // Optional pricing fields default to 0, so cache and reasoning cost nothing
84
+ expect(result.cacheRead).toBe(0);
85
+ expect(result.cacheWrite).toBe(0);
86
+ expect(result.reasoning).toBe(0);
87
+ expect(result.total).toBeCloseTo(0.006);
88
+ });
89
+
90
+ it("calculates full cost breakdown with all pricing fields", () => {
91
+ const usage: TokenUsage = {
92
+ inputTokens: 1000,
93
+ outputTokens: 500,
94
+ totalTokens: 1500,
95
+ cacheReadTokens: 200,
96
+ cacheWriteTokens: 100,
97
+ reasoningTokens: 300,
98
+ };
99
+
100
+ const result = calculateCost(usage, FULL_PRICING);
101
+
102
+ expect(result.input).toBeCloseTo(0.002);
103
+ expect(result.output).toBeCloseTo(0.004);
104
+ expect(result.cacheRead).toBeCloseTo(0.0001);
105
+ expect(result.cacheWrite).toBeCloseTo(0.0001);
106
+ expect(result.total).toBeCloseTo(0.0062);
107
+ });
108
+
109
+ it("calculates reasoning token costs when pricing is provided", () => {
110
+ const usage: TokenUsage = {
111
+ inputTokens: 1000,
112
+ outputTokens: 500,
113
+ totalTokens: 1500,
114
+ cacheReadTokens: 0,
115
+ cacheWriteTokens: 0,
116
+ reasoningTokens: 800,
117
+ };
118
+
119
+ const result = calculateCost(usage, REASONING_PRICING);
120
+
121
+ expect(result.reasoning).toBeCloseTo(0.0096);
122
+ expect(result.total).toBeCloseTo(0.002 + 0.004 + 0.0096);
123
+ });
124
+
125
+ it("defaults reasoning cost to zero when pricing omits reasoning", () => {
126
+ const usage: TokenUsage = {
127
+ inputTokens: 1000,
128
+ outputTokens: 500,
129
+ totalTokens: 1500,
130
+ cacheReadTokens: 0,
131
+ cacheWriteTokens: 0,
132
+ reasoningTokens: 800,
133
+ };
134
+
135
+ const result = calculateCost(usage, BASIC_PRICING);
136
+
137
+ expect(result.reasoning).toBe(0);
138
+ expect(result.total).toBeCloseTo(0.006);
139
+ });
140
+
141
+ it("total equals sum of all fields", () => {
142
+ const usage: TokenUsage = {
143
+ inputTokens: 500,
144
+ outputTokens: 250,
145
+ totalTokens: 750,
146
+ cacheReadTokens: 100,
147
+ cacheWriteTokens: 50,
148
+ reasoningTokens: 150,
149
+ };
150
+
151
+ const result = calculateCost(usage, FULL_PRICING);
152
+ const expectedTotal =
153
+ result.input + result.output + result.cacheRead + result.cacheWrite + result.reasoning;
154
+
155
+ expect(result.total).toBeCloseTo(expectedTotal);
156
+ });
157
+ });
@@ -0,0 +1,43 @@
1
+ import type { UsageCost } from "./types.js";
2
+ import type { ModelPricing } from "@/catalog/types.js";
3
+ import type { TokenUsage } from "@/provider/types.js";
4
+
5
+ /**
6
+ * Calculate the dollar cost from token usage and model pricing.
7
+ *
8
+ * Multiplies each token count by the corresponding per-token pricing rate.
9
+ * Optional pricing fields (cache) default to `0` when absent.
10
+ *
11
+ * @param usage - Token counts from a model invocation.
12
+ * @param pricing - Per-token pricing rates for the model.
13
+ * @returns A {@link UsageCost} with per-field and total costs in USD.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { calculateCost, model } from '@funkai/models'
18
+ *
19
+ * const usage: TokenUsage = {
20
+ * inputTokens: 1000,
21
+ * outputTokens: 500,
22
+ * totalTokens: 1500,
23
+ * cacheReadTokens: 200,
24
+ * cacheWriteTokens: 0,
25
+ * reasoningTokens: 0,
26
+ * }
27
+ * const m = model('gpt-4.1')
28
+ * if (m) {
29
+ * const cost = calculateCost(usage, m.pricing)
30
+ * console.log(cost.total) // 0.006
31
+ * }
32
+ * ```
33
+ */
34
+ export function calculateCost(usage: TokenUsage, pricing: ModelPricing): UsageCost {
35
+ const input = usage.inputTokens * pricing.input;
36
+ const output = usage.outputTokens * pricing.output;
37
+ const cacheRead = usage.cacheReadTokens * (pricing.cacheRead ?? 0);
38
+ const cacheWrite = usage.cacheWriteTokens * (pricing.cacheWrite ?? 0);
39
+ const reasoning = usage.reasoningTokens * (pricing.reasoning ?? 0);
40
+ const total = input + output + cacheRead + cacheWrite + reasoning;
41
+
42
+ return { input, output, cacheRead, cacheWrite, reasoning, total };
43
+ }
@@ -0,0 +1,2 @@
1
+ export { calculateCost } from "./calculate.js";
2
+ export type { UsageCost } from "./types.js";
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Breakdown of cost in USD from a model invocation.
3
+ *
4
+ * Each field is the dollar cost for that token category.
5
+ * All fields are non-negative numbers. Fields that don't apply are `0`.
6
+ */
7
+ export interface UsageCost {
8
+ /** Cost for input tokens. */
9
+ readonly input: number;
10
+
11
+ /** Cost for output tokens. */
12
+ readonly output: number;
13
+
14
+ /** Cost for cached input tokens (read). */
15
+ readonly cacheRead: number;
16
+
17
+ /** Cost for cached input tokens (write). */
18
+ readonly cacheWrite: number;
19
+
20
+ /** Cost for reasoning tokens. */
21
+ readonly reasoning: number;
22
+
23
+ /** Total cost (sum of all fields). */
24
+ readonly total: number;
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ // Catalog
2
+ export { model, models, MODELS } from "@/catalog/index.js";
3
+ export type {
4
+ KnownModelId,
5
+ ModelId,
6
+ ModelCapabilities,
7
+ ModelModalities,
8
+ ModelPricing,
9
+ ModelDefinition,
10
+ } from "@/catalog/index.js";
11
+
12
+ // Provider
13
+ export { createOpenRouter, openrouter } from "@/provider/openrouter.js";
14
+ export { createModelResolver } from "@/provider/resolver.js";
15
+ export type {
16
+ ProviderFactory,
17
+ ProviderMap,
18
+ ModelResolverConfig,
19
+ ModelResolver,
20
+ } from "@/provider/resolver.js";
21
+ export type { LanguageModel, TokenUsage } from "@/provider/types.js";
22
+
23
+ // Cost
24
+ export { calculateCost } from "@/cost/calculate.js";
25
+ export type { UsageCost } from "@/cost/types.js";
@@ -0,0 +1,9 @@
1
+ export { createOpenRouter, openrouter } from "./openrouter.js";
2
+ export { createModelResolver } from "./resolver.js";
3
+ export type {
4
+ ProviderFactory,
5
+ ProviderMap,
6
+ ModelResolverConfig,
7
+ ModelResolver,
8
+ } from "./resolver.js";
9
+ export type { LanguageModel, TokenUsage } from "./types.js";
@@ -0,0 +1,125 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ // Mock the external provider before importing the module under test
4
+ const mockBaseCreateOpenRouter = vi.fn();
5
+ const mockProviderInstance = vi.fn();
6
+
7
+ vi.mock("@openrouter/ai-sdk-provider", () => ({
8
+ createOpenRouter: mockBaseCreateOpenRouter,
9
+ }));
10
+
11
+ // Must import after mocking
12
+ const { createOpenRouter, openrouter } = await import("@/provider/openrouter.js");
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // createOpenRouter()
16
+ // ---------------------------------------------------------------------------
17
+
18
+ describe("createOpenRouter()", () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ mockBaseCreateOpenRouter.mockReturnValue(mockProviderInstance);
22
+ vi.stubEnv("OPENROUTER_API_KEY", "env-key-123");
23
+ });
24
+
25
+ afterEach(() => {
26
+ vi.unstubAllEnvs();
27
+ });
28
+
29
+ it("uses explicit apiKey when provided in options", () => {
30
+ createOpenRouter({ apiKey: "explicit-key" });
31
+
32
+ expect(mockBaseCreateOpenRouter).toHaveBeenCalledWith(
33
+ expect.objectContaining({ apiKey: "explicit-key" }),
34
+ );
35
+ });
36
+
37
+ it("falls back to OPENROUTER_API_KEY env var when no apiKey in options", () => {
38
+ createOpenRouter();
39
+
40
+ expect(mockBaseCreateOpenRouter).toHaveBeenCalledWith(
41
+ expect.objectContaining({ apiKey: "env-key-123" }),
42
+ );
43
+ });
44
+
45
+ it("falls back to env var when options are provided without apiKey", () => {
46
+ createOpenRouter({});
47
+
48
+ expect(mockBaseCreateOpenRouter).toHaveBeenCalledWith(
49
+ expect.objectContaining({ apiKey: "env-key-123" }),
50
+ );
51
+ });
52
+
53
+ it("throws when no apiKey provided and env var is not set", () => {
54
+ vi.stubEnv("OPENROUTER_API_KEY", undefined as unknown as string);
55
+
56
+ expect(() => createOpenRouter()).toThrow("OPENROUTER_API_KEY environment variable is required");
57
+ });
58
+
59
+ it("forwards additional options to the base provider", () => {
60
+ createOpenRouter({ apiKey: "key", baseURL: "https://custom.api" } as Record<string, unknown>);
61
+
62
+ expect(mockBaseCreateOpenRouter).toHaveBeenCalledWith(
63
+ expect.objectContaining({
64
+ apiKey: "key",
65
+ baseURL: "https://custom.api",
66
+ }),
67
+ );
68
+ });
69
+ });
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // openrouter() — cached model factory
73
+ // ---------------------------------------------------------------------------
74
+
75
+ describe("openrouter()", () => {
76
+ beforeEach(() => {
77
+ vi.clearAllMocks();
78
+ mockProviderInstance.mockReturnValue({ modelId: "mock-model" });
79
+ mockBaseCreateOpenRouter.mockReturnValue(mockProviderInstance);
80
+ });
81
+
82
+ afterEach(() => {
83
+ vi.unstubAllEnvs();
84
+ });
85
+
86
+ it("creates a provider and returns a language model", () => {
87
+ vi.stubEnv("OPENROUTER_API_KEY", "key-test-create");
88
+
89
+ const result = openrouter("openai/gpt-5.2-codex");
90
+
91
+ expect(mockBaseCreateOpenRouter).toHaveBeenCalledWith({ apiKey: "key-test-create" });
92
+ expect(mockProviderInstance).toHaveBeenCalledWith("openai/gpt-5.2-codex");
93
+ expect(result).toEqual({ modelId: "mock-model" });
94
+ });
95
+
96
+ it("reuses cached provider on subsequent calls with same api key", () => {
97
+ vi.stubEnv("OPENROUTER_API_KEY", "key-test-reuse");
98
+
99
+ openrouter("openai/gpt-5.2-codex");
100
+ openrouter("openai/gpt-5.2");
101
+
102
+ // Provider should only be created once since key is the same
103
+ expect(mockBaseCreateOpenRouter).toHaveBeenCalledTimes(1);
104
+ expect(mockProviderInstance).toHaveBeenCalledTimes(2);
105
+ });
106
+
107
+ it("creates new provider when api key changes", () => {
108
+ vi.stubEnv("OPENROUTER_API_KEY", "key-test-change-1");
109
+ openrouter("openai/gpt-5.2-codex");
110
+
111
+ vi.stubEnv("OPENROUTER_API_KEY", "key-test-change-2");
112
+ openrouter("openai/gpt-5.2");
113
+
114
+ expect(mockBaseCreateOpenRouter).toHaveBeenCalledTimes(2);
115
+ expect(mockBaseCreateOpenRouter).toHaveBeenLastCalledWith({ apiKey: "key-test-change-2" });
116
+ });
117
+
118
+ it("throws when OPENROUTER_API_KEY is not set", () => {
119
+ vi.stubEnv("OPENROUTER_API_KEY", undefined as unknown as string);
120
+
121
+ expect(() => openrouter("openai/gpt-5.2-codex")).toThrow(
122
+ "OPENROUTER_API_KEY environment variable is required",
123
+ );
124
+ });
125
+ });
@@ -0,0 +1,110 @@
1
+ import {
2
+ createOpenRouter as baseCreateOpenRouter,
3
+ type OpenRouterProvider,
4
+ type OpenRouterProviderSettings,
5
+ } from "@openrouter/ai-sdk-provider";
6
+
7
+ import type { ModelId } from "@/catalog/index.js";
8
+ import type { LanguageModel } from "@/provider/types.js";
9
+
10
+ /**
11
+ * Create an OpenRouter provider instance with automatic API key resolution.
12
+ *
13
+ * Falls back to the `OPENROUTER_API_KEY` environment variable when
14
+ * no explicit `apiKey` is provided in the options.
15
+ *
16
+ * @param options - Provider settings forwarded to `@openrouter/ai-sdk-provider`.
17
+ * @returns A configured {@link OpenRouterProvider} instance.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const openrouter = createOpenRouter({ apiKey: 'sk-...' })
22
+ * const m = openrouter('openai/gpt-5.2-codex')
23
+ * ```
24
+ */
25
+ export function createOpenRouter(options?: OpenRouterProviderSettings): OpenRouterProvider {
26
+ const apiKey = resolveApiKey(options);
27
+ return baseCreateOpenRouter({
28
+ ...options,
29
+ apiKey,
30
+ });
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Private helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * @private
39
+ */
40
+ function resolveApiKey(options: OpenRouterProviderSettings | undefined): string {
41
+ if (options != null && options.apiKey != null) {
42
+ return options.apiKey;
43
+ }
44
+ return getOpenRouterApiKey();
45
+ }
46
+
47
+ /**
48
+ * Create a cached OpenRouter model resolver.
49
+ *
50
+ * The returned function caches the underlying provider and invalidates
51
+ * when the API key changes at runtime.
52
+ *
53
+ * @returns A function that resolves a model ID to a {@link LanguageModel}.
54
+ *
55
+ * @private
56
+ */
57
+ // Cache mutation is safe: JS is single-threaded and baseCreateOpenRouter is synchronous.
58
+ // If this were refactored to async initialization, a lock or re-architecture would be needed.
59
+ function createCachedOpenRouter(): (modelId: ModelId) => LanguageModel {
60
+ const cache: { provider: OpenRouterProvider | undefined; apiKey: string | undefined } = {
61
+ provider: undefined,
62
+ apiKey: undefined,
63
+ };
64
+ return (modelId: ModelId): LanguageModel => {
65
+ const apiKey = getOpenRouterApiKey();
66
+ if (!cache.provider || cache.apiKey !== apiKey) {
67
+ cache.provider = baseCreateOpenRouter({ apiKey });
68
+ cache.apiKey = apiKey;
69
+ }
70
+ return cache.provider(modelId);
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Shorthand for creating a single OpenRouter language model.
76
+ *
77
+ * Resolves the API key from the environment and returns a ready-to-use
78
+ * {@link LanguageModel} that can be passed directly to AI SDK functions.
79
+ *
80
+ * The provider instance is cached at module scope and reused across
81
+ * calls. If `OPENROUTER_API_KEY` changes at runtime, the cache is
82
+ * invalidated and a new provider is created.
83
+ *
84
+ * @param modelId - A model identifier (e.g. `"openai/gpt-5.2-codex"`).
85
+ * @returns A configured {@link LanguageModel} instance.
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * const m = openrouter('openai/gpt-5.2-codex')
90
+ * ```
91
+ */
92
+ export const openrouter: (modelId: ModelId) => LanguageModel = createCachedOpenRouter();
93
+
94
+ /**
95
+ * Read the OpenRouter API key from the environment.
96
+ *
97
+ * @throws {Error} If `OPENROUTER_API_KEY` is not set.
98
+ *
99
+ * @private
100
+ */
101
+ function getOpenRouterApiKey(): string {
102
+ const apiKey = process.env.OPENROUTER_API_KEY;
103
+ if (!apiKey) {
104
+ throw new Error(
105
+ "OPENROUTER_API_KEY environment variable is required. " +
106
+ "Set it in your .env file or environment.",
107
+ );
108
+ }
109
+ return apiKey;
110
+ }
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import { createModelResolver } from "@/provider/resolver.js";
4
+ import type { LanguageModel } from "@/provider/types.js";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+
10
+ function fakeModel(id: string): LanguageModel {
11
+ return { modelId: id } as unknown as LanguageModel;
12
+ }
13
+
14
+ function fakeFactory(prefix: string): (modelName: string) => LanguageModel {
15
+ return (modelName: string) => fakeModel(`${prefix}/${modelName}`);
16
+ }
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // createModelResolver()
20
+ // ---------------------------------------------------------------------------
21
+
22
+ describe("createModelResolver()", () => {
23
+ it("throws when no providers or fallback are configured for prefixed ID", () => {
24
+ const resolve = createModelResolver();
25
+
26
+ expect(() => resolve("openai/gpt-4.1")).toThrow(
27
+ 'Cannot resolve model "openai/gpt-4.1": no provider mapped for "openai" and no fallback configured',
28
+ );
29
+ });
30
+
31
+ it("throws when no providers or fallback are configured for unprefixed ID", () => {
32
+ const resolve = createModelResolver();
33
+
34
+ expect(() => resolve("gpt-4.1")).toThrow(
35
+ 'Cannot resolve model "gpt-4.1": no provider prefix and no fallback configured',
36
+ );
37
+ });
38
+
39
+ it("uses a mapped provider when prefix matches", () => {
40
+ const openaiFactory = vi.fn(fakeFactory("openai"));
41
+ const resolve = createModelResolver({
42
+ providers: { openai: openaiFactory },
43
+ });
44
+
45
+ const result = resolve("openai/gpt-4.1");
46
+
47
+ expect(openaiFactory).toHaveBeenCalledWith("gpt-4.1");
48
+ expect(result).toEqual(fakeModel("openai/gpt-4.1"));
49
+ });
50
+
51
+ it("falls back to fallback for unmapped prefixes", () => {
52
+ const fallback = vi.fn((id: string) => fakeModel(id));
53
+ const resolve = createModelResolver({
54
+ providers: { openai: fakeFactory("openai") },
55
+ fallback,
56
+ });
57
+
58
+ resolve("anthropic/claude-sonnet-4-20250514");
59
+
60
+ expect(fallback).toHaveBeenCalledWith("anthropic/claude-sonnet-4-20250514");
61
+ });
62
+
63
+ it("handles model IDs without a slash via fallback", () => {
64
+ const fallback = vi.fn((id: string) => fakeModel(id));
65
+ const resolve = createModelResolver({ fallback });
66
+
67
+ resolve("gpt-4.1");
68
+
69
+ expect(fallback).toHaveBeenCalledWith("gpt-4.1");
70
+ });
71
+
72
+ it("throws for unmapped prefixes when no fallback is configured", () => {
73
+ const resolve = createModelResolver({
74
+ providers: { openai: fakeFactory("openai") },
75
+ });
76
+
77
+ expect(() => resolve("anthropic/claude-sonnet-4-20250514")).toThrow(
78
+ 'Cannot resolve model "anthropic/claude-sonnet-4-20250514": no provider mapped for "anthropic" and no fallback configured',
79
+ );
80
+ });
81
+
82
+ it("supports multiple mapped providers", () => {
83
+ const openaiFactory = vi.fn(fakeFactory("openai"));
84
+ const anthropicFactory = vi.fn(fakeFactory("anthropic"));
85
+
86
+ const resolve = createModelResolver({
87
+ providers: {
88
+ openai: openaiFactory,
89
+ anthropic: anthropicFactory,
90
+ },
91
+ });
92
+
93
+ resolve("openai/gpt-4.1");
94
+ resolve("anthropic/claude-sonnet-4-20250514");
95
+
96
+ expect(openaiFactory).toHaveBeenCalledWith("gpt-4.1");
97
+ expect(anthropicFactory).toHaveBeenCalledWith("claude-sonnet-4-20250514");
98
+ });
99
+
100
+ it("handles model IDs with multiple slashes correctly", () => {
101
+ const resolve = createModelResolver({
102
+ providers: { "meta-llama": fakeFactory("meta-llama") },
103
+ });
104
+
105
+ resolve("meta-llama/llama-4-scout/extended");
106
+
107
+ expect(resolve("meta-llama/llama-4-scout/extended")).toEqual(
108
+ fakeModel("meta-llama/llama-4-scout/extended"),
109
+ );
110
+ });
111
+
112
+ it("throws for empty model ID", () => {
113
+ const resolve = createModelResolver();
114
+
115
+ expect(() => resolve("")).toThrow("Cannot resolve model: model ID is empty");
116
+ });
117
+
118
+ it("throws for whitespace-only model ID", () => {
119
+ const resolve = createModelResolver();
120
+
121
+ expect(() => resolve(" ")).toThrow("Cannot resolve model: model ID is empty");
122
+ });
123
+
124
+ it("prefers mapped provider over fallback", () => {
125
+ const openaiFactory = vi.fn(fakeFactory("openai"));
126
+ const fallback = vi.fn((id: string) => fakeModel(id));
127
+
128
+ const resolve = createModelResolver({
129
+ providers: { openai: openaiFactory },
130
+ fallback,
131
+ });
132
+
133
+ resolve("openai/gpt-4.1");
134
+
135
+ expect(openaiFactory).toHaveBeenCalledWith("gpt-4.1");
136
+ expect(fallback).not.toHaveBeenCalled();
137
+ });
138
+ });