@funkai/models 0.1.0 → 0.3.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/.turbo/turbo-build.log +13 -9
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/dist/index.d.mts +40 -86
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +18 -114
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -19
- package/scripts/generate-models.ts +0 -20
- package/src/catalog/index.test.ts +0 -12
- package/src/cost/calculate.test.ts +0 -8
- package/src/index.ts +2 -8
- package/src/provider/index.ts +2 -8
- package/src/provider/registry.test.ts +87 -0
- package/src/provider/registry.ts +93 -0
- package/tsdown.config.ts +0 -1
- package/.turbo/turbo-typecheck.log +0 -4
- package/src/provider/openrouter.test.ts +0 -125
- package/src/provider/openrouter.ts +0 -110
- package/src/provider/resolver.test.ts +0 -138
- package/src/provider/resolver.ts +0 -125
|
@@ -1,110 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
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
|
-
});
|
package/src/provider/resolver.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import type { ModelId } from "@/catalog/index.js";
|
|
2
|
-
import type { LanguageModel } from "@/provider/types.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* A function that resolves a provider-specific model name to a {@link LanguageModel}.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```typescript
|
|
9
|
-
* import { createOpenAI } from '@ai-sdk/openai'
|
|
10
|
-
*
|
|
11
|
-
* const factory: ProviderFactory = createOpenAI({ apiKey: '...' })
|
|
12
|
-
* const model = factory('gpt-4.1')
|
|
13
|
-
* ```
|
|
14
|
-
*/
|
|
15
|
-
export type ProviderFactory = (modelName: string) => LanguageModel;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Mapping from provider prefix (e.g. `"openai"`, `"anthropic"`) to a factory
|
|
19
|
-
* that creates {@link LanguageModel} instances for that provider.
|
|
20
|
-
*/
|
|
21
|
-
export type ProviderMap = Readonly<Record<string, ProviderFactory>>;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Configuration for {@link createModelResolver}.
|
|
25
|
-
*/
|
|
26
|
-
export interface ModelResolverConfig {
|
|
27
|
-
/**
|
|
28
|
-
* Direct AI SDK provider mappings by prefix.
|
|
29
|
-
*
|
|
30
|
-
* When a model ID like `"openai/gpt-4.1"` is resolved, the prefix
|
|
31
|
-
* `"openai"` is extracted and looked up in this map. If found, the
|
|
32
|
-
* factory receives the model portion (`"gpt-4.1"`).
|
|
33
|
-
*/
|
|
34
|
-
readonly providers?: ProviderMap;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* A fallback factory used when no mapped provider matches.
|
|
38
|
-
*
|
|
39
|
-
* Receives the full model ID string. Useful for routing unmapped
|
|
40
|
-
* providers through an aggregator like OpenRouter.
|
|
41
|
-
*
|
|
42
|
-
* @example
|
|
43
|
-
* ```typescript
|
|
44
|
-
* import { openrouter } from '@funkai/models'
|
|
45
|
-
*
|
|
46
|
-
* const resolve = createModelResolver({
|
|
47
|
-
* providers: { openai: createOpenAI({ apiKey: '...' }) },
|
|
48
|
-
* fallback: openrouter,
|
|
49
|
-
* })
|
|
50
|
-
* ```
|
|
51
|
-
*/
|
|
52
|
-
readonly fallback?: (modelId: string) => LanguageModel;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* A function that resolves a model ID to a {@link LanguageModel} instance.
|
|
57
|
-
*/
|
|
58
|
-
export type ModelResolver = (modelId: ModelId) => LanguageModel;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Create a model resolver with configurable provider mappings.
|
|
62
|
-
*
|
|
63
|
-
* When a model ID like `"openai/gpt-4.1"` is resolved:
|
|
64
|
-
* 1. The provider prefix (`"openai"`) is extracted
|
|
65
|
-
* 2. If a direct provider is mapped for that prefix, it receives the model portion (`"gpt-4.1"`)
|
|
66
|
-
* 3. Otherwise, the fallback is called with the full ID (if configured)
|
|
67
|
-
* 4. If no fallback, an error is thrown
|
|
68
|
-
*
|
|
69
|
-
* @param config - Provider mappings and fallback configuration.
|
|
70
|
-
* @returns A resolver function that maps model IDs to {@link LanguageModel} instances.
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```typescript
|
|
74
|
-
* import { createOpenAI } from '@ai-sdk/openai'
|
|
75
|
-
* import { createAnthropic } from '@ai-sdk/anthropic'
|
|
76
|
-
*
|
|
77
|
-
* const resolve = createModelResolver({
|
|
78
|
-
* providers: {
|
|
79
|
-
* openai: createOpenAI({ apiKey: '...' }),
|
|
80
|
-
* anthropic: createAnthropic({ apiKey: '...' }),
|
|
81
|
-
* },
|
|
82
|
-
* })
|
|
83
|
-
*
|
|
84
|
-
* const m1 = resolve('openai/gpt-4.1') // uses @ai-sdk/openai directly
|
|
85
|
-
* const m2 = resolve('anthropic/claude-sonnet-4') // uses @ai-sdk/anthropic directly
|
|
86
|
-
* ```
|
|
87
|
-
*/
|
|
88
|
-
export function createModelResolver(config?: ModelResolverConfig): ModelResolver {
|
|
89
|
-
const providers = config?.providers ?? {};
|
|
90
|
-
const fallback = config?.fallback;
|
|
91
|
-
|
|
92
|
-
return (modelId: ModelId): LanguageModel => {
|
|
93
|
-
if (!modelId.trim()) {
|
|
94
|
-
throw new Error("Cannot resolve model: model ID is empty");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const slashIndex = modelId.indexOf("/");
|
|
98
|
-
|
|
99
|
-
if (slashIndex === -1) {
|
|
100
|
-
if (fallback) {
|
|
101
|
-
return fallback(modelId);
|
|
102
|
-
}
|
|
103
|
-
throw new Error(
|
|
104
|
-
`Cannot resolve model "${modelId}": no provider prefix and no fallback configured`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const prefix = modelId.slice(0, slashIndex);
|
|
109
|
-
// eslint-disable-next-line security/detect-object-injection -- Prefix extracted from user model ID, used as record key lookup
|
|
110
|
-
const factory = providers[prefix];
|
|
111
|
-
|
|
112
|
-
if (factory) {
|
|
113
|
-
const modelName = modelId.slice(slashIndex + 1);
|
|
114
|
-
return factory(modelName);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (fallback) {
|
|
118
|
-
return fallback(modelId);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
throw new Error(
|
|
122
|
-
`Cannot resolve model "${modelId}": no provider mapped for "${prefix}" and no fallback configured`,
|
|
123
|
-
);
|
|
124
|
-
};
|
|
125
|
-
}
|