@funkai/models 0.3.1 → 0.3.3

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.
@@ -53,6 +53,8 @@ interface ApiProvider {
53
53
  /**
54
54
  * Convert a provider key to a TypeScript constant name.
55
55
  * e.g. "openai" → "OPENAI_MODELS", "meta-llama" → "META_LLAMA_MODELS"
56
+ *
57
+ * @private
56
58
  */
57
59
  function toConstName(provider: string): string {
58
60
  return `${provider.toUpperCase().replaceAll(/[^A-Z0-9]/g, "_")}_MODELS`;
@@ -61,6 +63,8 @@ function toConstName(provider: string): string {
61
63
  /**
62
64
  * Lowercase the first character of a string, preserving the rest as-is.
63
65
  * e.g. "OpenAI" → "openAI", "GoogleVertex" → "googleVertex", "XAI" → "xAI"
66
+ *
67
+ * @private
64
68
  */
65
69
  function lowerFirst(s: string): string {
66
70
  if (s.length === 0) {
@@ -75,6 +79,8 @@ function lowerFirst(s: string): string {
75
79
 
76
80
  /**
77
81
  * Return the correct indefinite article ("a" or "an") for a word.
82
+ *
83
+ * @private
78
84
  */
79
85
  function article(word: string): string {
80
86
  if (/^[aeiou]/i.test(word)) {
@@ -86,6 +92,8 @@ function article(word: string): string {
86
92
  /**
87
93
  * Convert per-million-token rate to per-token rate, rounding to
88
94
  * eliminate floating-point noise (e.g. `8.000000000000001e-7`).
95
+ *
96
+ * @private
89
97
  */
90
98
  function toPerToken(perMillion: number): number {
91
99
  return parseFloat((perMillion / 1_000_000).toPrecision(6));
@@ -94,6 +102,8 @@ function toPerToken(perMillion: number): number {
94
102
  /**
95
103
  * Format a number for codegen output, using scientific notation for
96
104
  * very small values.
105
+ *
106
+ * @private
97
107
  */
98
108
  function fmtNum(n: number): string {
99
109
  if (n === 0) {
@@ -105,22 +115,29 @@ function fmtNum(n: number): string {
105
115
  return String(n);
106
116
  }
107
117
 
118
+ /** @private */
119
+ function extractExampleId(model: ApiModel | undefined): string {
120
+ if (model !== undefined && model !== null) {
121
+ return model.id;
122
+ }
123
+ return "example-id";
124
+ }
125
+
108
126
  /**
109
127
  * Build the pricing object literal string for a model.
128
+ *
129
+ * @private
110
130
  */
131
+ function extractCostField(cost: ApiModel["cost"], field: "input" | "output"): number {
132
+ if (cost !== undefined && cost !== null) {
133
+ return cost[field] ?? 0;
134
+ }
135
+ return 0;
136
+ }
137
+
111
138
  function buildPricing(cost: ApiModel["cost"]): string {
112
- const costInput: number = (() => {
113
- if (cost !== undefined && cost !== null && cost.input !== undefined && cost.input !== null) {
114
- return cost.input;
115
- }
116
- return 0;
117
- })();
118
- const costOutput: number = (() => {
119
- if (cost !== undefined && cost !== null && cost.output !== undefined && cost.output !== null) {
120
- return cost.output;
121
- }
122
- return 0;
123
- })();
139
+ const costInput = extractCostField(cost, "input");
140
+ const costOutput = extractCostField(cost, "output");
124
141
  const input = toPerToken(costInput);
125
142
  const output = toPerToken(costOutput);
126
143
  const parts: string[] = [`input: ${fmtNum(input)}`, `output: ${fmtNum(output)}`];
@@ -142,30 +159,22 @@ function buildPricing(cost: ApiModel["cost"]): string {
142
159
 
143
160
  /**
144
161
  * Build the modalities object literal string.
162
+ *
163
+ * @private
145
164
  */
165
+ function extractModalityField(
166
+ modalities: ApiModel["modalities"],
167
+ field: "input" | "output",
168
+ ): string[] {
169
+ if (modalities !== undefined && modalities !== null) {
170
+ return modalities[field] ?? ["text"];
171
+ }
172
+ return ["text"];
173
+ }
174
+
146
175
  function buildModalities(modalities: ApiModel["modalities"]): string {
147
- const modalInput: string[] = (() => {
148
- if (
149
- modalities !== undefined &&
150
- modalities !== null &&
151
- modalities.input !== undefined &&
152
- modalities.input !== null
153
- ) {
154
- return modalities.input;
155
- }
156
- return ["text"];
157
- })();
158
- const modalOutput: string[] = (() => {
159
- if (
160
- modalities !== undefined &&
161
- modalities !== null &&
162
- modalities.output !== undefined &&
163
- modalities.output !== null
164
- ) {
165
- return modalities.output;
166
- }
167
- return ["text"];
168
- })();
176
+ const modalInput = extractModalityField(modalities, "input");
177
+ const modalOutput = extractModalityField(modalities, "output");
169
178
  const input = JSON.stringify(modalInput);
170
179
  const output = JSON.stringify(modalOutput);
171
180
  return `{ input: ${input}, output: ${output} }`;
@@ -173,6 +182,8 @@ function buildModalities(modalities: ApiModel["modalities"]): string {
173
182
 
174
183
  /**
175
184
  * Build the capabilities object literal string.
185
+ *
186
+ * @private
176
187
  */
177
188
  function buildCapabilities(m: ApiModel): string {
178
189
  return [
@@ -185,28 +196,22 @@ function buildCapabilities(m: ApiModel): string {
185
196
 
186
197
  /**
187
198
  * Extract context window and max output from a model's limit field.
199
+ *
200
+ * @private
188
201
  */
189
202
  function getModelLimits(limit: ApiModel["limit"]): { contextWindow: number; maxOutput: number } {
190
203
  if (limit === undefined || limit === null) {
191
204
  return { contextWindow: 0, maxOutput: 0 };
192
205
  }
193
- const contextWindow: number = (() => {
194
- if (limit.context !== undefined && limit.context !== null) {
195
- return limit.context;
196
- }
197
- return 0;
198
- })();
199
- const maxOutput: number = (() => {
200
- if (limit.output !== undefined && limit.output !== null) {
201
- return limit.output;
202
- }
203
- return 0;
204
- })();
206
+ const contextWindow = limit.context ?? 0;
207
+ const maxOutput = limit.output ?? 0;
205
208
  return { contextWindow, maxOutput };
206
209
  }
207
210
 
208
211
  /**
209
212
  * Escape a string for use in a TypeScript single-quoted string literal.
213
+ *
214
+ * @private
210
215
  */
211
216
  function escapeStr(s: string): string {
212
217
  return s
@@ -216,6 +221,11 @@ function escapeStr(s: string): string {
216
221
  .replaceAll("\r", String.raw`\r`);
217
222
  }
218
223
 
224
+ /**
225
+ * Check whether the staleness cache file indicates a recent fetch.
226
+ *
227
+ * @private
228
+ */
219
229
  function isFresh(reqPath: string): boolean {
220
230
  if (!existsSync(reqPath)) {
221
231
  return false;
@@ -286,37 +296,33 @@ export default lauf({
286
296
  rmSync(ENTRY_DIR, { recursive: true, force: true });
287
297
  mkdirSync(ENTRY_DIR, { recursive: true });
288
298
 
289
- const providerFiles: { provider: string; constName: string; count: number }[] = [];
290
-
291
- for (const providerKey of providerKeys) {
299
+ const providerFiles = providerKeys.flatMap((providerKey) => {
292
300
  const apiProviderEntry = apiData[providerKey];
293
301
  const providerEntry = providers[providerKey];
294
- if (apiProviderEntry !== undefined && providerEntry !== undefined) {
295
- if (apiProviderEntry.models === undefined || apiProviderEntry.models === null) {
296
- throw new Error(
297
- `models.dev API returned no models for configured provider: ${providerKey}`,
298
- );
299
- }
300
- const apiModels = apiProviderEntry.models;
301
- const constName = toConstName(providerKey);
302
- const lines: string[] = [];
303
-
304
- for (const [, m] of Object.entries(apiModels)) {
305
- const id = escapeStr(m.id);
306
- const name = escapeStr(m.name ?? m.id);
307
- const family = escapeStr(m.family ?? "");
308
- const pricing = buildPricing(m.cost);
309
- const { contextWindow, maxOutput } = getModelLimits(m.limit);
310
- const modalities = buildModalities(m.modalities);
311
- const capabilities = buildCapabilities(m);
312
-
313
- lines.push(
314
- ` { id: '${id}', name: '${name}', provider: '${providerKey}', family: '${family}', pricing: ${pricing}, contextWindow: ${contextWindow}, maxOutput: ${maxOutput}, modalities: ${modalities}, capabilities: { ${capabilities} } },`,
315
- );
316
- }
317
-
318
- // Write catalog provider file
319
- const catalogContent = `${BANNER}
302
+ if (apiProviderEntry === undefined || providerEntry === undefined) {
303
+ return [];
304
+ }
305
+ if (apiProviderEntry.models === undefined || apiProviderEntry.models === null) {
306
+ throw new Error(
307
+ `models.dev API returned no models for configured provider: ${providerKey}`,
308
+ );
309
+ }
310
+ const apiModels = apiProviderEntry.models;
311
+ const constName = toConstName(providerKey);
312
+ const lines = Object.values(apiModels).map((m) => {
313
+ const id = escapeStr(m.id);
314
+ const name = escapeStr(m.name ?? m.id);
315
+ const family = escapeStr(m.family ?? "");
316
+ const pricing = buildPricing(m.cost);
317
+ const { contextWindow, maxOutput } = getModelLimits(m.limit);
318
+ const modalities = buildModalities(m.modalities);
319
+ const capabilities = buildCapabilities(m);
320
+
321
+ return ` { id: '${id}', name: '${name}', provider: '${providerKey}', family: '${family}', pricing: ${pricing}, contextWindow: ${contextWindow}, maxOutput: ${maxOutput}, modalities: ${modalities}, capabilities: { ${capabilities} } },`;
322
+ });
323
+
324
+ // Write catalog provider file
325
+ const catalogContent = `${BANNER}
320
326
 
321
327
  import type { ModelDefinition } from '../types.js'
322
328
 
@@ -325,24 +331,17 @@ ${lines.join("\n")}
325
331
  ] as const satisfies readonly ModelDefinition[]
326
332
  `;
327
333
 
328
- const catalogPath = join(CATALOG_DIR, `${providerKey}.ts`);
329
- writeFileSync(catalogPath, catalogContent, "utf8");
330
-
331
- // Write per-provider entry point
332
- const { prefix } = providerEntry;
333
- const camel = lowerFirst(prefix);
334
- const [firstModel] = Object.values(apiModels);
335
- const exampleId = escapeStr(
336
- (() => {
337
- if (firstModel !== undefined) {
338
- return firstModel.id;
339
- }
340
- return "example-id";
341
- })(),
342
- );
343
- const providerName = escapeStr(providerEntry.name);
344
- const art = article(providerEntry.name);
345
- const entryContent = `${BANNER}
334
+ const catalogPath = join(CATALOG_DIR, `${providerKey}.ts`);
335
+ writeFileSync(catalogPath, catalogContent, "utf8");
336
+
337
+ // Write per-provider entry point
338
+ const { prefix } = providerEntry;
339
+ const camel = lowerFirst(prefix);
340
+ const [firstModel] = Object.values(apiModels);
341
+ const exampleId = escapeStr(extractExampleId(firstModel));
342
+ const providerName = escapeStr(providerEntry.name);
343
+ const art = article(providerEntry.name);
344
+ const entryContent = `${BANNER}
346
345
 
347
346
  import type { LiteralUnion } from 'type-fest'
348
347
  import type { ModelDefinition } from '../catalog/types.js'
@@ -374,6 +373,7 @@ export type ${prefix}ModelId = (typeof ${constName})[number]['id']
374
373
  */
375
374
  export const ${camel}Models = ${constName}
376
375
 
376
+ /** @private */
377
377
  const MODEL_INDEX = new Map<string, ModelDefinition>(${constName}.map((m) => [m.id, m]))
378
378
 
379
379
  /**
@@ -397,13 +397,12 @@ export function ${camel}Model(id: LiteralUnion<${prefix}ModelId, string>): Model
397
397
  }
398
398
  `;
399
399
 
400
- const entryPath = join(ENTRY_DIR, `${providerKey}.ts`);
401
- writeFileSync(entryPath, entryContent, "utf8");
400
+ const entryPath = join(ENTRY_DIR, `${providerKey}.ts`);
401
+ writeFileSync(entryPath, entryContent, "utf8");
402
402
 
403
- ctx.logger.success(`${providerKey} (${lines.length} models)`);
404
- providerFiles.push({ provider: providerKey, constName, count: lines.length });
405
- }
406
- }
403
+ ctx.logger.success(`${providerKey} (${lines.length} models)`);
404
+ return [{ provider: providerKey, constName, count: lines.length }];
405
+ });
407
406
 
408
407
  // Catalog barrel
409
408
  const imports = providerFiles
@@ -414,6 +413,7 @@ export function ${camel}Model(id: LiteralUnion<${prefix}ModelId, string>): Model
414
413
 
415
414
  const catalogBarrel = `${BANNER}
416
415
 
416
+ // oxlint-disable eslint-plugin-import/max-dependencies
417
417
  import type { ModelDefinition } from '../types.js'
418
418
  ${imports}
419
419
 
@@ -439,15 +439,17 @@ ${spreads}
439
439
  types: "./dist/index.d.mts",
440
440
  import: "./dist/index.mjs",
441
441
  },
442
+ ...Object.fromEntries(
443
+ providerFiles.map((p) => [
444
+ `./${p.provider}`,
445
+ {
446
+ types: `./dist/providers/${p.provider}.d.mts`,
447
+ import: `./dist/providers/${p.provider}.mjs`,
448
+ },
449
+ ]),
450
+ ),
442
451
  };
443
452
 
444
- for (const p of providerFiles) {
445
- exportsMap[`./${p.provider}`] = {
446
- types: `./dist/providers/${p.provider}.d.mts`,
447
- import: `./dist/providers/${p.provider}.mjs`,
448
- };
449
- }
450
-
451
453
  pkg.exports = exportsMap;
452
454
  writeFileSync(PACKAGE_JSON_PATH, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
453
455
  ctx.logger.success("package.json exports map updated");
@@ -23,6 +23,7 @@ export type ModelId = LiteralUnion<KnownModelId, string>;
23
23
  */
24
24
  export const MODELS = GENERATED_MODELS satisfies readonly ModelDefinition[];
25
25
 
26
+ /** @private */
26
27
  const MODEL_INDEX = new Map<string, ModelDefinition>(MODELS.map((m) => [m.id, m]));
27
28
 
28
29
  /**
@@ -8,6 +8,12 @@ import type { TokenUsage } from "@/provider/types.js";
8
8
  * Multiplies each token count by the corresponding per-token pricing rate.
9
9
  * Optional pricing fields (cache) default to `0` when absent.
10
10
  *
11
+ * **Reasoning token semantics**: `reasoningTokens` in {@link TokenUsage} are
12
+ * expected to be **exclusive** of `outputTokens` — they are billed separately.
13
+ * If your provider includes reasoning tokens _within_ the `outputTokens` count,
14
+ * you must deduct them before passing usage to this function to avoid
15
+ * double-counting output costs.
16
+ *
11
17
  * @param usage - Token counts from a model invocation.
12
18
  * @param pricing - Per-token pricing rates for the model.
13
19
  * @returns A {@link UsageCost} with per-field and total costs in USD.
package/src/cost/types.ts CHANGED
@@ -3,6 +3,10 @@
3
3
  *
4
4
  * Each field is the dollar cost for that token category.
5
5
  * All fields are non-negative numbers. Fields that don't apply are `0`.
6
+ *
7
+ * **Note**: Values may exhibit floating-point imprecision inherent to
8
+ * JavaScript `number` (IEEE 754 double-precision) arithmetic. Do not rely
9
+ * on exact equality comparisons against expected cost values.
6
10
  */
7
11
  export interface UsageCost {
8
12
  /** Cost for input tokens. */
@@ -59,6 +59,8 @@ export type ProviderRegistry = (modelId: ModelId) => LanguageModel;
59
59
  *
60
60
  * @param config - Provider mappings.
61
61
  * @returns A resolver function that maps model IDs to {@link LanguageModel} instances.
62
+ * @throws {Error} If the model ID is empty, missing the `provider/model` format,
63
+ * or the underlying AI SDK registry fails to resolve the provider or model.
62
64
  *
63
65
  * @example
64
66
  * ```typescript
@@ -86,8 +88,40 @@ export function createProviderRegistry(config: ProviderRegistryConfig): Provider
86
88
  if (!modelId.trim()) {
87
89
  throw new Error("Cannot resolve model: model ID is empty");
88
90
  }
91
+ if (!modelId.includes("/")) {
92
+ throw new Error(
93
+ `Invalid model ID "${modelId}": expected "provider/model" format (e.g. "openai/gpt-4.1")`,
94
+ );
95
+ }
89
96
  // Cast needed: AI SDK overloads expect `provider/model` template literal,
90
97
  // But our ModelId is a branded string union. The runtime validates the format.
91
- return inner.languageModel(modelId as `${string}/${string}`) as LanguageModel;
98
+ try {
99
+ // SAFETY: The first cast (`as \`${string}/${string}\``) is safe because we
100
+ // Validated above that `modelId` contains `/`. The second cast (`as
101
+ // LanguageModel`) narrows from the AI SDK's broader LanguageModel union to
102
+ // The v3 specification, which is the only version we support.
103
+ return inner.languageModel(modelId as `${string}/${string}`) as LanguageModel;
104
+ } catch (error) {
105
+ throw new Error(`Failed to resolve model "${modelId}": ${errorMessage(error)}`, {
106
+ cause: error,
107
+ });
108
+ }
92
109
  };
93
110
  }
111
+
112
+ // ---------------------------------------------------------------------------
113
+
114
+ /**
115
+ * Extract a human-readable message from an unknown error value.
116
+ *
117
+ * @param error - The caught error value.
118
+ * @returns The error message string.
119
+ *
120
+ * @private
121
+ */
122
+ function errorMessage(error: unknown): string {
123
+ if (error instanceof Error) {
124
+ return error.message;
125
+ }
126
+ return String(error);
127
+ }
@@ -17,6 +17,12 @@ export type LanguageModel = Extract<BaseLanguageModel, { specificationVersion: "
17
17
  * Base token counts shared by raw tracking records and final output.
18
18
  *
19
19
  * All fields are resolved `number` (0 when absent).
20
+ *
21
+ * **Reasoning token semantics**: `reasoningTokens` must be **exclusive** of
22
+ * `outputTokens`. Some providers include reasoning tokens inside the output
23
+ * token count — if so, callers must deduct reasoning tokens from
24
+ * `outputTokens` before constructing this type to avoid double-counting in
25
+ * cost calculations.
20
26
  */
21
27
  export interface TokenUsage {
22
28
  /** Number of input (prompt) tokens. */
package/tsconfig.json CHANGED
@@ -5,21 +5,28 @@
5
5
  "module": "NodeNext",
6
6
  "moduleResolution": "NodeNext",
7
7
  "lib": ["ES2024"],
8
+ "types": ["node"],
9
+
8
10
  "strict": true,
9
- "esModuleInterop": true,
11
+ "noUncheckedIndexedAccess": true,
12
+ "exactOptionalPropertyTypes": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "noPropertyAccessFromIndexSignature": true,
15
+
16
+ "verbatimModuleSyntax": true,
17
+ "resolveJsonModule": true,
10
18
  "skipLibCheck": true,
11
19
  "forceConsistentCasingInFileNames": true,
12
- "resolveJsonModule": true,
13
- "isolatedModules": true,
20
+
14
21
  "declaration": true,
15
22
  "declarationMap": true,
16
23
  "sourceMap": true,
24
+
17
25
  "outDir": "./dist",
18
26
  "rootDir": ".",
19
27
  "paths": {
20
28
  "@/*": ["./src/*"]
21
- },
22
- "types": ["node"]
29
+ }
23
30
  },
24
31
  "include": ["src"],
25
32
  "exclude": ["node_modules", "dist"]