@nebutra/ai-providers 1.0.1
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/LICENSE +21 -0
- package/README.frontier.md +35 -0
- package/README.md +46 -0
- package/dist/catalog.d.ts +128 -0
- package/dist/catalog.js +312 -0
- package/dist/catalog.js.map +1 -0
- package/dist/frontier.d.ts +191 -0
- package/dist/frontier.js +183 -0
- package/dist/frontier.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +783 -0
- package/dist/index.js.map +1 -0
- package/dist/meta.d.ts +25 -0
- package/dist/meta.js +474 -0
- package/dist/meta.js.map +1 -0
- package/package.json +82 -0
- package/templates/.env.example.template +28 -0
- package/templates/registry.ts.template +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wuxi Nebutra Intelligence Technology Co., Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Frontier model SSOT
|
|
2
|
+
|
|
3
|
+
**Single source of truth:** [`src/frontier.ts`](./src/frontier.ts)
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_PUBLIC_MODEL,
|
|
8
|
+
DEFAULT_PREFIXED_MODEL,
|
|
9
|
+
FRONTIER,
|
|
10
|
+
ROUTER_PUBLIC_MODEL_IDS,
|
|
11
|
+
AGENT_MODEL_PRESETS,
|
|
12
|
+
} from "@nebutra/ai-providers/frontier";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## How to bump the industry catalog
|
|
16
|
+
|
|
17
|
+
1. Edit **only** `packages/ai/ai-providers/src/frontier.ts` (slot values + prices).
|
|
18
|
+
2. Run tests: `pnpm --filter @nebutra/ai-providers test` (if configured) and consumers.
|
|
19
|
+
3. Do **not** hand-type versioned model strings in apps/gateway/router/forge.
|
|
20
|
+
|
|
21
|
+
## Who consumes it
|
|
22
|
+
|
|
23
|
+
| Consumer | Import |
|
|
24
|
+
|----------|--------|
|
|
25
|
+
| Router supply aliases | `@nebutra/ai-providers/frontier` → `DEFAULT_ALIASES` |
|
|
26
|
+
| Forge price card | same → `REF_PRICE_CARD` |
|
|
27
|
+
| Agents presets / defaultModel | `AGENT_MODEL_PRESETS` / `DEFAULT_PREFIXED_MODEL` |
|
|
28
|
+
| Gateway AI routes | `DEFAULT_PUBLIC_MODEL` |
|
|
29
|
+
| Router app fallbacks | `@nebutra/router-supply` re-export of default |
|
|
30
|
+
| DB offline seed | `ROUTER_PUBLIC_MODEL_IDS` + prices |
|
|
31
|
+
|
|
32
|
+
## Live vs offline
|
|
33
|
+
|
|
34
|
+
- **Offline / product defaults:** this registry.
|
|
35
|
+
- **Live “newest routable”:** `resolveFrontierModel(tier)` in `catalog.ts` (OpenRouter ∩ models.dev), falling back to `FRONTIER_TIER_FALLBACK`.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @nebutra/ai-providers
|
|
2
|
+
|
|
3
|
+
Provider registry metadata for Nebutra AI scaffolding.
|
|
4
|
+
|
|
5
|
+
This package is **stable** because it is metadata-only. It does not execute model
|
|
6
|
+
requests, open network connections, or own runtime retry/fallback behavior.
|
|
7
|
+
Runtime AI calls live in `@nebutra/agents`.
|
|
8
|
+
|
|
9
|
+
## What This Package Exports
|
|
10
|
+
|
|
11
|
+
- `PROVIDERS` — the full provider metadata registry.
|
|
12
|
+
- `PROVIDERS_BY_CATEGORY` — grouped provider metadata for UI/docs rendering.
|
|
13
|
+
- `getProvider(id)` — lookup by provider id.
|
|
14
|
+
- `isCNCompatible(provider)` — deployment-region compatibility helper.
|
|
15
|
+
- `ProviderMeta`, `ProviderStatus`, and `ProviderCategory` types.
|
|
16
|
+
- `templates/registry.ts.template` — consumed by `create-sailor` when generating
|
|
17
|
+
app-local provider registries.
|
|
18
|
+
|
|
19
|
+
## Runtime Boundary
|
|
20
|
+
|
|
21
|
+
Use `@nebutra/agents` for runtime calls such as model creation, streaming,
|
|
22
|
+
generation, embedding, and provider fallback execution.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { getProvider, PROVIDERS } from "@nebutra/ai-providers";
|
|
26
|
+
|
|
27
|
+
const openai = getProvider("openai");
|
|
28
|
+
const cnProviders = PROVIDERS.filter((provider) => provider.status === "cn-compatible");
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Consumers
|
|
32
|
+
|
|
33
|
+
- `create-sailor` uses this registry to scaffold AI provider configuration.
|
|
34
|
+
- Documentation generators use it to render provider support matrices.
|
|
35
|
+
- Admin/product UIs may use it to display supported provider metadata.
|
|
36
|
+
|
|
37
|
+
## Maintenance Rules
|
|
38
|
+
|
|
39
|
+
- Keep this package data-only. Do not add runtime provider SDK clients here.
|
|
40
|
+
- Add network/runtime behavior to `@nebutra/agents`.
|
|
41
|
+
- When adding a provider, update `src/meta.ts` and the templates that expose the
|
|
42
|
+
provider to generated apps.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
AGPL-3.0
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model catalog — the single source of model/provider FACTS.
|
|
3
|
+
*
|
|
4
|
+
* Data comes from the upstream community catalog https://models.dev (via
|
|
5
|
+
* `@tokenlens/fetch`), so model ids, providers, pricing, context windows and
|
|
6
|
+
* capabilities are NOT hand-maintained here. Replaces the scattered hardcoded
|
|
7
|
+
* lists (BYOK provider regex, the /models endpoint, seed fallbacks).
|
|
8
|
+
*
|
|
9
|
+
* Delivery: runtime fetch + in-memory TTL cache. The first call fetches; later
|
|
10
|
+
* calls serve from cache until `MODEL_CATALOG_TTL_MS` (default 6h) elapses. On
|
|
11
|
+
* fetch failure the last good catalog is reused, so a models.dev outage degrades
|
|
12
|
+
* to "slightly stale" rather than "down". Accessors are async by design.
|
|
13
|
+
*/
|
|
14
|
+
/** Our coarse provider buckets — one BYOK key + base URL per bucket. */
|
|
15
|
+
type AIProviderId = "OPENAI" | "ANTHROPIC" | "GOOGLE" | "SILICONFLOW" | "CUSTOM";
|
|
16
|
+
/**
|
|
17
|
+
* Map models.dev provider IDs → our AIProvider buckets. Canonical home (the DB
|
|
18
|
+
* seed imports this, instead of keeping a parallel copy). Unknown → CUSTOM.
|
|
19
|
+
*/
|
|
20
|
+
declare const PROVIDER_MAP: Record<string, AIProviderId>;
|
|
21
|
+
declare function mapProvider(modelsDevProviderId: string): AIProviderId;
|
|
22
|
+
/**
|
|
23
|
+
* Output modality — the primary dimension for canvas orchestration ("give me a
|
|
24
|
+
* video model", "an image model", …). Derived from models.dev `modalities.output`.
|
|
25
|
+
*/
|
|
26
|
+
type ModelModality = "text" | "image" | "video" | "audio" | "embedding";
|
|
27
|
+
/** Normalized, provider-agnostic model facts surfaced to the rest of the app. */
|
|
28
|
+
interface ModelInfo {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
/** Our coarse bucket (for BYOK key selection / base URL). */
|
|
32
|
+
provider: AIProviderId;
|
|
33
|
+
/** The raw models.dev provider id (e.g. "google-vertex"). */
|
|
34
|
+
rawProvider: string;
|
|
35
|
+
/** What the model produces — the canvas-orchestration grouping key. */
|
|
36
|
+
modality: ModelModality;
|
|
37
|
+
contextWindow?: number;
|
|
38
|
+
maxOutput?: number;
|
|
39
|
+
pricing?: {
|
|
40
|
+
inputPerMTok?: number;
|
|
41
|
+
outputPerMTok?: number;
|
|
42
|
+
};
|
|
43
|
+
capabilities: {
|
|
44
|
+
reasoning: boolean;
|
|
45
|
+
toolCall: boolean;
|
|
46
|
+
vision: boolean;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** A single provider's offering of a logical model. */
|
|
50
|
+
interface ModelOffering {
|
|
51
|
+
/** Routable model id as listed by models.dev (aggregator-prefixed). */
|
|
52
|
+
id: string;
|
|
53
|
+
rawProvider: string;
|
|
54
|
+
bucket: AIProviderId;
|
|
55
|
+
pricing?: {
|
|
56
|
+
inputPerMTok?: number;
|
|
57
|
+
outputPerMTok?: number;
|
|
58
|
+
};
|
|
59
|
+
contextWindow?: number;
|
|
60
|
+
}
|
|
61
|
+
/** One deduped logical model, with all the providers that serve it. */
|
|
62
|
+
interface CatalogModel {
|
|
63
|
+
key: string;
|
|
64
|
+
name: string;
|
|
65
|
+
modality: ModelModality;
|
|
66
|
+
capabilities: {
|
|
67
|
+
reasoning: boolean;
|
|
68
|
+
toolCall: boolean;
|
|
69
|
+
vision: boolean;
|
|
70
|
+
};
|
|
71
|
+
/** Distinct provider buckets that serve it (for BYOK key selection). */
|
|
72
|
+
buckets: AIProviderId[];
|
|
73
|
+
offerings: ModelOffering[];
|
|
74
|
+
}
|
|
75
|
+
/** Full model list (newest fetch, cached). */
|
|
76
|
+
declare function listModels(): Promise<ModelInfo[]>;
|
|
77
|
+
/**
|
|
78
|
+
* Deduped logical models for a given output modality — the canvas-orchestration
|
|
79
|
+
* entry point. One entry per logical model, with the providers that serve it.
|
|
80
|
+
* Sorted by offering count (most widely available first).
|
|
81
|
+
*/
|
|
82
|
+
declare function listModelsByModality(modality: ModelModality): Promise<CatalogModel[]>;
|
|
83
|
+
/** Count of deduped logical models per modality (for canvas tab badges, etc.). */
|
|
84
|
+
declare function listModalities(): Promise<Record<ModelModality, number>>;
|
|
85
|
+
/** Look up a single model by id. `null` if unknown to the catalog. */
|
|
86
|
+
declare function getModelInfo(modelId: string): Promise<ModelInfo | null>;
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the coarse provider bucket for a model id. Catalog-first; falls back
|
|
89
|
+
* to id-prefix heuristics for brand-new models models.dev hasn't indexed yet.
|
|
90
|
+
* `null` means "unknown" → callers should use the platform default.
|
|
91
|
+
*/
|
|
92
|
+
declare function providerForModel(modelId: string): Promise<AIProviderId | null>;
|
|
93
|
+
/** Last-resort id-prefix heuristic (only used when the catalog misses). */
|
|
94
|
+
declare function providerFromIdHeuristic(modelId: string): AIProviderId | null;
|
|
95
|
+
type ModelTier = "reasoning" | "flagship" | "fast" | "openai-flagship" | "google-flagship" | "google-fast";
|
|
96
|
+
/** The hardcoded frontier fallbacks, exported so consumers can mirror them. */
|
|
97
|
+
declare const FRONTIER_FALLBACK: Record<ModelTier, string>;
|
|
98
|
+
/**
|
|
99
|
+
* Resolve a semantic tier to the newest concrete model id that is BOTH
|
|
100
|
+
* gateway-routable and present in the models.dev catalog. Falls back to the
|
|
101
|
+
* audited hardcoded frontier when the live lists are unavailable.
|
|
102
|
+
*/
|
|
103
|
+
declare function resolveFrontierModel(tier: ModelTier): Promise<string>;
|
|
104
|
+
interface ModelSpecHints {
|
|
105
|
+
/** Explicit pin — wins over every hint. Preset alias or `provider/model` id. */
|
|
106
|
+
id?: string;
|
|
107
|
+
reasoningEffort?: "low" | "medium" | "high" | "xhigh";
|
|
108
|
+
modality?: ModelModality;
|
|
109
|
+
capabilities?: {
|
|
110
|
+
vision?: boolean;
|
|
111
|
+
toolCall?: boolean;
|
|
112
|
+
reasoning?: boolean;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve a per-node model spec to a concrete model id. Precedence: explicit
|
|
117
|
+
* `id` → non-text `modality` (most-available matching model) → `reasoningEffort`
|
|
118
|
+
* tier (text frontier) → text `capabilities` match → `fallback` (the run
|
|
119
|
+
* default). Never throws — degrades to `fallback`.
|
|
120
|
+
*/
|
|
121
|
+
declare function resolveModelSpec(spec: ModelSpecHints, fallback: string): Promise<string>;
|
|
122
|
+
/**
|
|
123
|
+
* Default OpenAI-compatible base URL per provider bucket (env-overridable).
|
|
124
|
+
* Single home for these — BYOK and any other caller import from here.
|
|
125
|
+
*/
|
|
126
|
+
declare function providerBaseUrl(provider: AIProviderId): string | null;
|
|
127
|
+
|
|
128
|
+
export { type AIProviderId, type CatalogModel, FRONTIER_FALLBACK, type ModelInfo, type ModelModality, type ModelOffering, type ModelSpecHints, type ModelTier, PROVIDER_MAP, getModelInfo, listModalities, listModels, listModelsByModality, mapProvider, providerBaseUrl, providerForModel, providerFromIdHeuristic, resolveFrontierModel, resolveModelSpec };
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { fetchModels } from '@tokenlens/fetch';
|
|
2
|
+
|
|
3
|
+
// src/catalog.ts
|
|
4
|
+
var PROVIDER_MAP = {
|
|
5
|
+
openai: "OPENAI",
|
|
6
|
+
anthropic: "ANTHROPIC",
|
|
7
|
+
google: "GOOGLE",
|
|
8
|
+
"google-vertex": "GOOGLE",
|
|
9
|
+
"google-vertex-anthropic": "ANTHROPIC",
|
|
10
|
+
"amazon-bedrock": "ANTHROPIC",
|
|
11
|
+
// most bedrock usage is claude
|
|
12
|
+
siliconflow: "SILICONFLOW",
|
|
13
|
+
"siliconflow-cn": "SILICONFLOW",
|
|
14
|
+
// SiliconFlow ecosystem (GLM / Qwen / DeepSeek / MiniMax / …)
|
|
15
|
+
deepseek: "SILICONFLOW",
|
|
16
|
+
// deepseek is siliconflow-compatible
|
|
17
|
+
alibaba: "SILICONFLOW",
|
|
18
|
+
"alibaba-token-plan": "SILICONFLOW",
|
|
19
|
+
qwen: "SILICONFLOW",
|
|
20
|
+
moonshot: "SILICONFLOW",
|
|
21
|
+
zhipu: "SILICONFLOW",
|
|
22
|
+
minimax: "SILICONFLOW"
|
|
23
|
+
};
|
|
24
|
+
function mapProvider(modelsDevProviderId) {
|
|
25
|
+
return PROVIDER_MAP[modelsDevProviderId] ?? "CUSTOM";
|
|
26
|
+
}
|
|
27
|
+
function classifyModality(model) {
|
|
28
|
+
const out = model.modalities?.output ?? [];
|
|
29
|
+
if (out.includes("video")) return "video";
|
|
30
|
+
if (out.includes("image")) return "image";
|
|
31
|
+
if (out.includes("audio")) return "audio";
|
|
32
|
+
if (/embedding/i.test(model.id) || out.includes("embedding")) return "embedding";
|
|
33
|
+
return "text";
|
|
34
|
+
}
|
|
35
|
+
function toModelInfo(rawProvider, model) {
|
|
36
|
+
const visionIn = model.modalities?.input?.includes("image") ?? false;
|
|
37
|
+
return {
|
|
38
|
+
id: model.id,
|
|
39
|
+
name: model.name ?? model.id,
|
|
40
|
+
provider: mapProvider(rawProvider),
|
|
41
|
+
rawProvider,
|
|
42
|
+
modality: classifyModality(model),
|
|
43
|
+
...model.limit?.context !== void 0 ? { contextWindow: model.limit.context } : {},
|
|
44
|
+
...model.limit?.output !== void 0 ? { maxOutput: model.limit.output } : {},
|
|
45
|
+
pricing: {
|
|
46
|
+
...model.cost?.input !== void 0 ? { inputPerMTok: model.cost.input } : {},
|
|
47
|
+
...model.cost?.output !== void 0 ? { outputPerMTok: model.cost.output } : {}
|
|
48
|
+
},
|
|
49
|
+
capabilities: {
|
|
50
|
+
reasoning: model.reasoning ?? false,
|
|
51
|
+
toolCall: model.tool_call ?? false,
|
|
52
|
+
vision: visionIn
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function dedupKey(modelId) {
|
|
57
|
+
return modelId.toLowerCase().replace(/^.*\//, "").replace(/[._:\-\s]/g, "");
|
|
58
|
+
}
|
|
59
|
+
var TTL_MS = (() => {
|
|
60
|
+
const n = Number.parseInt(process.env.MODEL_CATALOG_TTL_MS ?? "21600000", 10);
|
|
61
|
+
return Number.isFinite(n) && n > 0 ? n : 216e5;
|
|
62
|
+
})();
|
|
63
|
+
var cache = null;
|
|
64
|
+
var inflight = null;
|
|
65
|
+
function buildIndex(catalog) {
|
|
66
|
+
const index = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const [providerId, providerInfo] of Object.entries(catalog)) {
|
|
68
|
+
for (const model of Object.values(providerInfo.models)) {
|
|
69
|
+
if (!index.has(model.id)) index.set(model.id, toModelInfo(providerId, model));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return index;
|
|
73
|
+
}
|
|
74
|
+
function buildLogical(catalog) {
|
|
75
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const [providerId, providerInfo] of Object.entries(catalog)) {
|
|
77
|
+
for (const model of Object.values(providerInfo.models)) {
|
|
78
|
+
const key = dedupKey(model.id);
|
|
79
|
+
if (!key) continue;
|
|
80
|
+
const bucket = mapProvider(providerId);
|
|
81
|
+
const offering = {
|
|
82
|
+
id: model.id,
|
|
83
|
+
rawProvider: providerId,
|
|
84
|
+
bucket,
|
|
85
|
+
pricing: {
|
|
86
|
+
...model.cost?.input !== void 0 ? { inputPerMTok: model.cost.input } : {},
|
|
87
|
+
...model.cost?.output !== void 0 ? { outputPerMTok: model.cost.output } : {}
|
|
88
|
+
},
|
|
89
|
+
...model.limit?.context !== void 0 ? { contextWindow: model.limit.context } : {}
|
|
90
|
+
};
|
|
91
|
+
const existing = byKey.get(key);
|
|
92
|
+
if (existing) {
|
|
93
|
+
existing.offerings.push(offering);
|
|
94
|
+
if (!existing.buckets.includes(bucket)) existing.buckets.push(bucket);
|
|
95
|
+
existing.capabilities.reasoning ||= model.reasoning ?? false;
|
|
96
|
+
existing.capabilities.toolCall ||= model.tool_call ?? false;
|
|
97
|
+
existing.capabilities.vision ||= model.modalities?.input?.includes("image") ?? false;
|
|
98
|
+
} else {
|
|
99
|
+
byKey.set(key, {
|
|
100
|
+
key,
|
|
101
|
+
name: model.name ?? model.id,
|
|
102
|
+
modality: classifyModality(model),
|
|
103
|
+
capabilities: {
|
|
104
|
+
reasoning: model.reasoning ?? false,
|
|
105
|
+
toolCall: model.tool_call ?? false,
|
|
106
|
+
vision: model.modalities?.input?.includes("image") ?? false
|
|
107
|
+
},
|
|
108
|
+
buckets: [bucket],
|
|
109
|
+
offerings: [offering]
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [...byKey.values()];
|
|
115
|
+
}
|
|
116
|
+
async function loadCache() {
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
if (cache && now - cache.fetchedAt < TTL_MS) return cache;
|
|
119
|
+
if (inflight) return inflight;
|
|
120
|
+
inflight = (async () => {
|
|
121
|
+
try {
|
|
122
|
+
const catalog = await fetchModels();
|
|
123
|
+
cache = { index: buildIndex(catalog), logical: buildLogical(catalog), fetchedAt: Date.now() };
|
|
124
|
+
return cache;
|
|
125
|
+
} catch {
|
|
126
|
+
if (cache) return cache;
|
|
127
|
+
cache = { index: /* @__PURE__ */ new Map(), logical: [], fetchedAt: Date.now() };
|
|
128
|
+
return cache;
|
|
129
|
+
} finally {
|
|
130
|
+
inflight = null;
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
return inflight;
|
|
134
|
+
}
|
|
135
|
+
async function loadIndex() {
|
|
136
|
+
return (await loadCache()).index;
|
|
137
|
+
}
|
|
138
|
+
async function listModels() {
|
|
139
|
+
return [...(await loadIndex()).values()];
|
|
140
|
+
}
|
|
141
|
+
async function listModelsByModality(modality) {
|
|
142
|
+
const { logical } = await loadCache();
|
|
143
|
+
return logical.filter((m) => m.modality === modality).sort((a, b) => b.offerings.length - a.offerings.length);
|
|
144
|
+
}
|
|
145
|
+
async function listModalities() {
|
|
146
|
+
const { logical } = await loadCache();
|
|
147
|
+
const counts = {
|
|
148
|
+
text: 0,
|
|
149
|
+
image: 0,
|
|
150
|
+
video: 0,
|
|
151
|
+
audio: 0,
|
|
152
|
+
embedding: 0
|
|
153
|
+
};
|
|
154
|
+
for (const m of logical) counts[m.modality]++;
|
|
155
|
+
return counts;
|
|
156
|
+
}
|
|
157
|
+
async function getModelInfo(modelId) {
|
|
158
|
+
return (await loadIndex()).get(modelId) ?? null;
|
|
159
|
+
}
|
|
160
|
+
async function providerForModel(modelId) {
|
|
161
|
+
const info = await getModelInfo(modelId);
|
|
162
|
+
if (info) return info.provider;
|
|
163
|
+
return providerFromIdHeuristic(modelId);
|
|
164
|
+
}
|
|
165
|
+
function providerFromIdHeuristic(modelId) {
|
|
166
|
+
const m = modelId.toLowerCase().replace(/^[^/]+\//, "");
|
|
167
|
+
if (/^(gpt-|o1|o3|o4|chatgpt|text-|davinci|babbage)/.test(m)) return "OPENAI";
|
|
168
|
+
if (m.startsWith("claude")) return "ANTHROPIC";
|
|
169
|
+
if (m.startsWith("gemini")) return "GOOGLE";
|
|
170
|
+
if (/deepseek|qwen|glm|yi-|internlm|siliconflow|moonshot|kimi/.test(m)) return "SILICONFLOW";
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
var TIER_RULES = {
|
|
174
|
+
reasoning: {
|
|
175
|
+
include: /^anthropic\/claude-opus-/,
|
|
176
|
+
exclude: /-(fast|mini|nano|image|codex)/,
|
|
177
|
+
fallback: "anthropic/claude-opus-4.8"
|
|
178
|
+
},
|
|
179
|
+
flagship: {
|
|
180
|
+
include: /^anthropic\/claude-sonnet-/,
|
|
181
|
+
exclude: /-(fast|mini|nano|image|codex)/,
|
|
182
|
+
fallback: "anthropic/claude-sonnet-4.6"
|
|
183
|
+
},
|
|
184
|
+
fast: {
|
|
185
|
+
include: /^anthropic\/claude-haiku-/,
|
|
186
|
+
exclude: /-(image|codex)/,
|
|
187
|
+
fallback: "anthropic/claude-haiku-4.5"
|
|
188
|
+
},
|
|
189
|
+
"openai-flagship": {
|
|
190
|
+
include: /^openai\/gpt-5/,
|
|
191
|
+
// Prefer full flagship over mini/nano/chat/image/codex variants
|
|
192
|
+
exclude: /-(pro|mini|nano|codex|image|chat)|gpt-5\.\d+-(mini|nano|pro)/,
|
|
193
|
+
// models.dev current OpenAI flagship (audited 2026-07); live resolver may pick newer
|
|
194
|
+
fallback: "openai/gpt-5.5"
|
|
195
|
+
},
|
|
196
|
+
"google-flagship": {
|
|
197
|
+
include: /^google\/gemini-.*pro/,
|
|
198
|
+
exclude: /-(image|tts|customtools)/,
|
|
199
|
+
fallback: "google/gemini-3.1-pro-preview"
|
|
200
|
+
},
|
|
201
|
+
"google-fast": {
|
|
202
|
+
include: /^google\/gemini-.*flash/,
|
|
203
|
+
exclude: /-(image|tts|lite)/,
|
|
204
|
+
// models.dev: gemini-3.5-flash / 3.6-flash class
|
|
205
|
+
fallback: "google/gemini-3.5-flash"
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
var FRONTIER_FALLBACK = Object.fromEntries(
|
|
209
|
+
Object.entries(TIER_RULES).map(([tier, rule]) => [tier, rule.fallback])
|
|
210
|
+
);
|
|
211
|
+
var OPENROUTER_MODELS_URL = process.env.OPENROUTER_MODELS_URL ?? "https://openrouter.ai/api/v1/models";
|
|
212
|
+
var routableCache = null;
|
|
213
|
+
var routableInflight = null;
|
|
214
|
+
async function fetchRoutableIds() {
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
if (routableCache && now - routableCache.fetchedAt < TTL_MS) return routableCache.ids;
|
|
217
|
+
if (routableInflight) return routableInflight;
|
|
218
|
+
routableInflight = (async () => {
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch(OPENROUTER_MODELS_URL);
|
|
221
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
222
|
+
const json = await res.json();
|
|
223
|
+
const ids = new Set(
|
|
224
|
+
(json.data ?? []).map((m) => m.id).filter((x) => typeof x === "string")
|
|
225
|
+
);
|
|
226
|
+
routableCache = { ids, fetchedAt: Date.now() };
|
|
227
|
+
return ids;
|
|
228
|
+
} catch {
|
|
229
|
+
return routableCache?.ids ?? /* @__PURE__ */ new Set();
|
|
230
|
+
} finally {
|
|
231
|
+
routableInflight = null;
|
|
232
|
+
}
|
|
233
|
+
})();
|
|
234
|
+
return routableInflight;
|
|
235
|
+
}
|
|
236
|
+
var collapseId = (s) => s.toLowerCase().replace(/[._-]/g, "");
|
|
237
|
+
var bareId = (id) => {
|
|
238
|
+
const slash = id.indexOf("/");
|
|
239
|
+
return slash >= 0 ? id.slice(slash + 1) : id;
|
|
240
|
+
};
|
|
241
|
+
var versionOf = (id) => {
|
|
242
|
+
const m = bareId(id).match(/(\d+(?:\.\d+)?)/);
|
|
243
|
+
const raw = m?.[1];
|
|
244
|
+
return raw ? Number.parseFloat(raw) : -1;
|
|
245
|
+
};
|
|
246
|
+
async function resolveFrontierModel(tier) {
|
|
247
|
+
const rule = TIER_RULES[tier];
|
|
248
|
+
try {
|
|
249
|
+
const [routable, metaIndex] = await Promise.all([fetchRoutableIds(), loadIndex()]);
|
|
250
|
+
if (routable.size === 0) return rule.fallback;
|
|
251
|
+
const metaNorm = new Set([...metaIndex.keys()].map(collapseId));
|
|
252
|
+
const candidates = [...routable].filter(
|
|
253
|
+
(id) => rule.include.test(id) && !rule.exclude.test(id) && metaNorm.has(collapseId(bareId(id)))
|
|
254
|
+
);
|
|
255
|
+
if (candidates.length === 0) return rule.fallback;
|
|
256
|
+
candidates.sort((a, b) => versionOf(b) - versionOf(a));
|
|
257
|
+
return candidates[0] ?? rule.fallback;
|
|
258
|
+
} catch {
|
|
259
|
+
return rule.fallback;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
var EFFORT_TIER = {
|
|
263
|
+
low: "fast",
|
|
264
|
+
medium: "flagship",
|
|
265
|
+
high: "flagship",
|
|
266
|
+
xhigh: "reasoning"
|
|
267
|
+
};
|
|
268
|
+
function pickByCapabilities(models, caps) {
|
|
269
|
+
const filtered = caps ? models.filter(
|
|
270
|
+
(m) => (!caps.vision || m.capabilities.vision) && (!caps.toolCall || m.capabilities.toolCall) && (!caps.reasoning || m.capabilities.reasoning)
|
|
271
|
+
) : models;
|
|
272
|
+
return filtered[0] ?? null;
|
|
273
|
+
}
|
|
274
|
+
async function resolveModelSpec(spec, fallback) {
|
|
275
|
+
try {
|
|
276
|
+
if (spec.id) return spec.id;
|
|
277
|
+
if (spec.modality && spec.modality !== "text") {
|
|
278
|
+
const match = pickByCapabilities(
|
|
279
|
+
await listModelsByModality(spec.modality),
|
|
280
|
+
spec.capabilities
|
|
281
|
+
);
|
|
282
|
+
if (match) return match.offerings[0]?.id ?? match.key;
|
|
283
|
+
return fallback;
|
|
284
|
+
}
|
|
285
|
+
if (spec.reasoningEffort) return resolveFrontierModel(EFFORT_TIER[spec.reasoningEffort]);
|
|
286
|
+
if (spec.capabilities) {
|
|
287
|
+
const match = pickByCapabilities(await listModelsByModality("text"), spec.capabilities);
|
|
288
|
+
if (match) return match.offerings[0]?.id ?? match.key;
|
|
289
|
+
}
|
|
290
|
+
return fallback;
|
|
291
|
+
} catch {
|
|
292
|
+
return fallback;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function providerBaseUrl(provider) {
|
|
296
|
+
switch (provider) {
|
|
297
|
+
case "OPENAI":
|
|
298
|
+
return process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
|
|
299
|
+
case "ANTHROPIC":
|
|
300
|
+
return process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com/v1";
|
|
301
|
+
case "GOOGLE":
|
|
302
|
+
return process.env.GOOGLE_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta/openai";
|
|
303
|
+
case "SILICONFLOW":
|
|
304
|
+
return process.env.SILICONFLOW_BASE_URL ?? "https://api.siliconflow.cn/v1";
|
|
305
|
+
default:
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export { FRONTIER_FALLBACK, PROVIDER_MAP, getModelInfo, listModalities, listModels, listModelsByModality, mapProvider, providerBaseUrl, providerForModel, providerFromIdHeuristic, resolveFrontierModel, resolveModelSpec };
|
|
311
|
+
//# sourceMappingURL=catalog.js.map
|
|
312
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/catalog.ts"],"names":[],"mappings":"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","file":"catalog.js","sourcesContent":["/**\n * Model catalog — the single source of model/provider FACTS.\n *\n * Data comes from the upstream community catalog https://models.dev (via\n * `@tokenlens/fetch`), so model ids, providers, pricing, context windows and\n * capabilities are NOT hand-maintained here. Replaces the scattered hardcoded\n * lists (BYOK provider regex, the /models endpoint, seed fallbacks).\n *\n * Delivery: runtime fetch + in-memory TTL cache. The first call fetches; later\n * calls serve from cache until `MODEL_CATALOG_TTL_MS` (default 6h) elapses. On\n * fetch failure the last good catalog is reused, so a models.dev outage degrades\n * to \"slightly stale\" rather than \"down\". Accessors are async by design.\n */\n\nimport type { ProviderModel, ProvidersCatalog } from \"@tokenlens/core\";\nimport { fetchModels } from \"@tokenlens/fetch\";\n\n/** Our coarse provider buckets — one BYOK key + base URL per bucket. */\nexport type AIProviderId = \"OPENAI\" | \"ANTHROPIC\" | \"GOOGLE\" | \"SILICONFLOW\" | \"CUSTOM\";\n\n/**\n * Map models.dev provider IDs → our AIProvider buckets. Canonical home (the DB\n * seed imports this, instead of keeping a parallel copy). Unknown → CUSTOM.\n */\nexport const PROVIDER_MAP: Record<string, AIProviderId> = {\n openai: \"OPENAI\",\n anthropic: \"ANTHROPIC\",\n google: \"GOOGLE\",\n \"google-vertex\": \"GOOGLE\",\n \"google-vertex-anthropic\": \"ANTHROPIC\",\n \"amazon-bedrock\": \"ANTHROPIC\", // most bedrock usage is claude\n siliconflow: \"SILICONFLOW\",\n \"siliconflow-cn\": \"SILICONFLOW\", // SiliconFlow ecosystem (GLM / Qwen / DeepSeek / MiniMax / …)\n deepseek: \"SILICONFLOW\", // deepseek is siliconflow-compatible\n alibaba: \"SILICONFLOW\",\n \"alibaba-token-plan\": \"SILICONFLOW\",\n qwen: \"SILICONFLOW\",\n moonshot: \"SILICONFLOW\",\n zhipu: \"SILICONFLOW\",\n minimax: \"SILICONFLOW\",\n};\n\nexport function mapProvider(modelsDevProviderId: string): AIProviderId {\n return PROVIDER_MAP[modelsDevProviderId] ?? \"CUSTOM\";\n}\n\n/**\n * Output modality — the primary dimension for canvas orchestration (\"give me a\n * video model\", \"an image model\", …). Derived from models.dev `modalities.output`.\n */\nexport type ModelModality = \"text\" | \"image\" | \"video\" | \"audio\" | \"embedding\";\n\n/** Classify a model by what it PRODUCES (not what it consumes). */\nfunction classifyModality(model: ProviderModel): ModelModality {\n const out = model.modalities?.output ?? [];\n if (out.includes(\"video\")) return \"video\";\n if (out.includes(\"image\")) return \"image\";\n if (out.includes(\"audio\")) return \"audio\";\n if (/embedding/i.test(model.id) || out.includes(\"embedding\")) return \"embedding\";\n return \"text\";\n}\n\n/** Normalized, provider-agnostic model facts surfaced to the rest of the app. */\nexport interface ModelInfo {\n id: string;\n name: string;\n /** Our coarse bucket (for BYOK key selection / base URL). */\n provider: AIProviderId;\n /** The raw models.dev provider id (e.g. \"google-vertex\"). */\n rawProvider: string;\n /** What the model produces — the canvas-orchestration grouping key. */\n modality: ModelModality;\n contextWindow?: number;\n maxOutput?: number;\n pricing?: { inputPerMTok?: number; outputPerMTok?: number };\n capabilities: { reasoning: boolean; toolCall: boolean; vision: boolean };\n}\n\nfunction toModelInfo(rawProvider: string, model: ProviderModel): ModelInfo {\n const visionIn = model.modalities?.input?.includes(\"image\") ?? false;\n return {\n id: model.id,\n name: model.name ?? model.id,\n provider: mapProvider(rawProvider),\n rawProvider,\n modality: classifyModality(model),\n ...(model.limit?.context !== undefined ? { contextWindow: model.limit.context } : {}),\n ...(model.limit?.output !== undefined ? { maxOutput: model.limit.output } : {}),\n pricing: {\n ...(model.cost?.input !== undefined ? { inputPerMTok: model.cost.input } : {}),\n ...(model.cost?.output !== undefined ? { outputPerMTok: model.cost.output } : {}),\n },\n capabilities: {\n reasoning: model.reasoning ?? false,\n toolCall: model.tool_call ?? false,\n vision: visionIn,\n },\n };\n}\n\n// ─── Deduped, modality-grouped view (for canvas orchestration) ─────────────────\n//\n// models.dev lists the SAME logical model under dozens of aggregator providers\n// (e.g. minimax-m2.5 appears under 47). The canvas needs ONE entry per logical\n// model with the list of providers that can serve it (so it can pick a routable\n// id + compare cost). `dedupKey` collapses the bare name across aggregators.\n\n/** A single provider's offering of a logical model. */\nexport interface ModelOffering {\n /** Routable model id as listed by models.dev (aggregator-prefixed). */\n id: string;\n rawProvider: string;\n bucket: AIProviderId;\n pricing?: { inputPerMTok?: number; outputPerMTok?: number };\n contextWindow?: number;\n}\n\n/** One deduped logical model, with all the providers that serve it. */\nexport interface CatalogModel {\n key: string;\n name: string;\n modality: ModelModality;\n capabilities: { reasoning: boolean; toolCall: boolean; vision: boolean };\n /** Distinct provider buckets that serve it (for BYOK key selection). */\n buckets: AIProviderId[];\n offerings: ModelOffering[];\n}\n\n/** Collapse a model id to a logical key (drop aggregator path + separators). */\nfunction dedupKey(modelId: string): string {\n return modelId\n .toLowerCase()\n .replace(/^.*\\//, \"\") // keep only the final path segment\n .replace(/[._:\\-\\s]/g, \"\");\n}\n\n// ─── Runtime cache ────────────────────────────────────────────────────────────\n\ninterface CacheEntry {\n /** Per-id view (exact lookups: providerForModel, getModelInfo, resolveFrontier). */\n index: Map<string, ModelInfo>;\n /** Deduped logical view (canvas orchestration: listModelsByModality). */\n logical: CatalogModel[];\n fetchedAt: number;\n}\n\nconst TTL_MS = (() => {\n const n = Number.parseInt(process.env.MODEL_CATALOG_TTL_MS ?? \"21600000\", 10); // 6h\n return Number.isFinite(n) && n > 0 ? n : 21_600_000;\n})();\n\nlet cache: CacheEntry | null = null;\nlet inflight: Promise<CacheEntry> | null = null;\n\nfunction buildIndex(catalog: ProvidersCatalog): Map<string, ModelInfo> {\n const index = new Map<string, ModelInfo>();\n for (const [providerId, providerInfo] of Object.entries(catalog)) {\n for (const model of Object.values(providerInfo.models)) {\n // First writer wins; model ids are unique within models.dev.\n if (!index.has(model.id)) index.set(model.id, toModelInfo(providerId, model));\n }\n }\n return index;\n}\n\nfunction buildLogical(catalog: ProvidersCatalog): CatalogModel[] {\n const byKey = new Map<string, CatalogModel>();\n for (const [providerId, providerInfo] of Object.entries(catalog)) {\n for (const model of Object.values(providerInfo.models)) {\n const key = dedupKey(model.id);\n if (!key) continue;\n const bucket = mapProvider(providerId);\n const offering: ModelOffering = {\n id: model.id,\n rawProvider: providerId,\n bucket,\n pricing: {\n ...(model.cost?.input !== undefined ? { inputPerMTok: model.cost.input } : {}),\n ...(model.cost?.output !== undefined ? { outputPerMTok: model.cost.output } : {}),\n },\n ...(model.limit?.context !== undefined ? { contextWindow: model.limit.context } : {}),\n };\n const existing = byKey.get(key);\n if (existing) {\n existing.offerings.push(offering);\n if (!existing.buckets.includes(bucket)) existing.buckets.push(bucket);\n // Capabilities are the union across offerings.\n existing.capabilities.reasoning ||= model.reasoning ?? false;\n existing.capabilities.toolCall ||= model.tool_call ?? false;\n existing.capabilities.vision ||= model.modalities?.input?.includes(\"image\") ?? false;\n } else {\n byKey.set(key, {\n key,\n name: model.name ?? model.id,\n modality: classifyModality(model),\n capabilities: {\n reasoning: model.reasoning ?? false,\n toolCall: model.tool_call ?? false,\n vision: model.modalities?.input?.includes(\"image\") ?? false,\n },\n buckets: [bucket],\n offerings: [offering],\n });\n }\n }\n }\n return [...byKey.values()];\n}\n\nasync function loadCache(): Promise<CacheEntry> {\n const now = Date.now();\n if (cache && now - cache.fetchedAt < TTL_MS) return cache;\n if (inflight) return inflight;\n\n inflight = (async () => {\n try {\n const catalog = await fetchModels();\n cache = { index: buildIndex(catalog), logical: buildLogical(catalog), fetchedAt: Date.now() };\n return cache;\n } catch {\n // models.dev unreachable — reuse the last good catalog if we have one,\n // otherwise empty (callers degrade safely, e.g. BYOK → platform).\n if (cache) return cache;\n cache = { index: new Map(), logical: [], fetchedAt: Date.now() };\n return cache;\n } finally {\n inflight = null;\n }\n })();\n\n return inflight;\n}\n\nasync function loadIndex(): Promise<Map<string, ModelInfo>> {\n return (await loadCache()).index;\n}\n\n// ─── Public accessors ─────────────────────────────────────────────────────────\n\n/** Full model list (newest fetch, cached). */\nexport async function listModels(): Promise<ModelInfo[]> {\n return [...(await loadIndex()).values()];\n}\n\n/**\n * Deduped logical models for a given output modality — the canvas-orchestration\n * entry point. One entry per logical model, with the providers that serve it.\n * Sorted by offering count (most widely available first).\n */\nexport async function listModelsByModality(modality: ModelModality): Promise<CatalogModel[]> {\n const { logical } = await loadCache();\n return logical\n .filter((m) => m.modality === modality)\n .sort((a, b) => b.offerings.length - a.offerings.length);\n}\n\n/** Count of deduped logical models per modality (for canvas tab badges, etc.). */\nexport async function listModalities(): Promise<Record<ModelModality, number>> {\n const { logical } = await loadCache();\n const counts: Record<ModelModality, number> = {\n text: 0,\n image: 0,\n video: 0,\n audio: 0,\n embedding: 0,\n };\n for (const m of logical) counts[m.modality]++;\n return counts;\n}\n\n/** Look up a single model by id. `null` if unknown to the catalog. */\nexport async function getModelInfo(modelId: string): Promise<ModelInfo | null> {\n return (await loadIndex()).get(modelId) ?? null;\n}\n\n/**\n * Resolve the coarse provider bucket for a model id. Catalog-first; falls back\n * to id-prefix heuristics for brand-new models models.dev hasn't indexed yet.\n * `null` means \"unknown\" → callers should use the platform default.\n */\nexport async function providerForModel(modelId: string): Promise<AIProviderId | null> {\n const info = await getModelInfo(modelId);\n if (info) return info.provider;\n return providerFromIdHeuristic(modelId);\n}\n\n/** Last-resort id-prefix heuristic (only used when the catalog misses). */\nexport function providerFromIdHeuristic(modelId: string): AIProviderId | null {\n const m = modelId.toLowerCase().replace(/^[^/]+\\//, \"\"); // strip \"anthropic/\" style prefix\n if (/^(gpt-|o1|o3|o4|chatgpt|text-|davinci|babbage)/.test(m)) return \"OPENAI\";\n if (m.startsWith(\"claude\")) return \"ANTHROPIC\";\n if (m.startsWith(\"gemini\")) return \"GOOGLE\";\n if (/deepseek|qwen|glm|yi-|internlm|siliconflow|moonshot|kimi/.test(m)) return \"SILICONFLOW\";\n return null;\n}\n\n// ─── Frontier resolver (hybrid: live intersection + hardcoded fallback) ─────────\n//\n// Resolves a SEMANTIC tier (e.g. \"the latest reasoning model\") to a concrete\n// model id, picking the newest from the INTERSECTION of:\n// - gateway-routable ids (OpenRouter /api/v1/models — the gateway namespace), and\n// - models.dev catalog metadata (so the pick has pricing/context/capabilities).\n// On any failure (offline / empty) it returns the hardcoded fallback, so callers\n// always get a usable id. This is the \"don't hand-maintain model strings\" engine.\n\nexport type ModelTier =\n | \"reasoning\"\n | \"flagship\"\n | \"fast\"\n | \"openai-flagship\"\n | \"google-flagship\"\n | \"google-fast\";\n\ninterface TierRule {\n /** Match on the OpenRouter (gateway) id. */\n include: RegExp;\n /** Drop non-frontier variants (fast/mini/codex/image/…). */\n exclude: RegExp;\n /** Current frontier (audited 2026-06-05) — used when the live lists are unavailable. */\n fallback: string;\n}\n\nconst TIER_RULES: Record<ModelTier, TierRule> = {\n reasoning: {\n include: /^anthropic\\/claude-opus-/,\n exclude: /-(fast|mini|nano|image|codex)/,\n fallback: \"anthropic/claude-opus-4.8\",\n },\n flagship: {\n include: /^anthropic\\/claude-sonnet-/,\n exclude: /-(fast|mini|nano|image|codex)/,\n fallback: \"anthropic/claude-sonnet-4.6\",\n },\n fast: {\n include: /^anthropic\\/claude-haiku-/,\n exclude: /-(image|codex)/,\n fallback: \"anthropic/claude-haiku-4.5\",\n },\n \"openai-flagship\": {\n include: /^openai\\/gpt-5/,\n // Prefer full flagship over mini/nano/chat/image/codex variants\n exclude: /-(pro|mini|nano|codex|image|chat)|gpt-5\\.\\d+-(mini|nano|pro)/,\n // models.dev current OpenAI flagship (audited 2026-07); live resolver may pick newer\n fallback: \"openai/gpt-5.5\",\n },\n \"google-flagship\": {\n include: /^google\\/gemini-.*pro/,\n exclude: /-(image|tts|customtools)/,\n fallback: \"google/gemini-3.1-pro-preview\",\n },\n \"google-fast\": {\n include: /^google\\/gemini-.*flash/,\n exclude: /-(image|tts|lite)/,\n // models.dev: gemini-3.5-flash / 3.6-flash class\n fallback: \"google/gemini-3.5-flash\",\n },\n};\n\n/** The hardcoded frontier fallbacks, exported so consumers can mirror them. */\nexport const FRONTIER_FALLBACK: Record<ModelTier, string> = Object.fromEntries(\n Object.entries(TIER_RULES).map(([tier, rule]) => [tier, rule.fallback]),\n) as Record<ModelTier, string>;\n\nconst OPENROUTER_MODELS_URL =\n process.env.OPENROUTER_MODELS_URL ?? \"https://openrouter.ai/api/v1/models\";\n\nlet routableCache: { ids: Set<string>; fetchedAt: number } | null = null;\nlet routableInflight: Promise<Set<string>> | null = null;\n\n/** Gateway-routable model ids (OpenRouter), cached with the same TTL as the catalog. */\nasync function fetchRoutableIds(): Promise<Set<string>> {\n const now = Date.now();\n if (routableCache && now - routableCache.fetchedAt < TTL_MS) return routableCache.ids;\n if (routableInflight) return routableInflight;\n\n routableInflight = (async () => {\n try {\n const res = await fetch(OPENROUTER_MODELS_URL);\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const json = (await res.json()) as { data?: Array<{ id?: string }> };\n const ids = new Set(\n (json.data ?? []).map((m) => m.id).filter((x): x is string => typeof x === \"string\"),\n );\n routableCache = { ids, fetchedAt: Date.now() };\n return ids;\n } catch {\n return routableCache?.ids ?? new Set<string>();\n } finally {\n routableInflight = null;\n }\n })();\n\n return routableInflight;\n}\n\n/** Collapse separators so `claude-opus-4.8` (OpenRouter) matches `claude-opus-4-8` (models.dev). */\nconst collapseId = (s: string): string => s.toLowerCase().replace(/[._-]/g, \"\");\nconst bareId = (id: string): string => {\n const slash = id.indexOf(\"/\");\n return slash >= 0 ? id.slice(slash + 1) : id;\n};\nconst versionOf = (id: string): number => {\n const m = bareId(id).match(/(\\d+(?:\\.\\d+)?)/);\n const raw = m?.[1];\n return raw ? Number.parseFloat(raw) : -1;\n};\n\n/**\n * Resolve a semantic tier to the newest concrete model id that is BOTH\n * gateway-routable and present in the models.dev catalog. Falls back to the\n * audited hardcoded frontier when the live lists are unavailable.\n */\nexport async function resolveFrontierModel(tier: ModelTier): Promise<string> {\n const rule = TIER_RULES[tier];\n try {\n const [routable, metaIndex] = await Promise.all([fetchRoutableIds(), loadIndex()]);\n if (routable.size === 0) return rule.fallback;\n\n const metaNorm = new Set([...metaIndex.keys()].map(collapseId));\n const candidates = [...routable].filter(\n (id) =>\n rule.include.test(id) && !rule.exclude.test(id) && metaNorm.has(collapseId(bareId(id))),\n );\n if (candidates.length === 0) return rule.fallback;\n\n candidates.sort((a, b) => versionOf(b) - versionOf(a));\n return candidates[0] ?? rule.fallback;\n } catch {\n return rule.fallback;\n }\n}\n\n// ─── Per-node model spec resolution ─────────────────────────────────────────────\n//\n// Resolves a node's model HINTS (id pin OR modality/effort/capability) to a\n// concrete model id. Mirrors agent-runtime NodeModelSpec structurally (plain\n// object — no cross-package dependency). Used by the workflow/agent executor.\n\nexport interface ModelSpecHints {\n /** Explicit pin — wins over every hint. Preset alias or `provider/model` id. */\n id?: string;\n reasoningEffort?: \"low\" | \"medium\" | \"high\" | \"xhigh\";\n modality?: ModelModality;\n capabilities?: { vision?: boolean; toolCall?: boolean; reasoning?: boolean };\n}\n\nconst EFFORT_TIER: Record<NonNullable<ModelSpecHints[\"reasoningEffort\"]>, ModelTier> = {\n low: \"fast\",\n medium: \"flagship\",\n high: \"flagship\",\n xhigh: \"reasoning\",\n};\n\nfunction pickByCapabilities(\n models: CatalogModel[],\n caps?: ModelSpecHints[\"capabilities\"],\n): CatalogModel | null {\n const filtered = caps\n ? models.filter(\n (m) =>\n (!caps.vision || m.capabilities.vision) &&\n (!caps.toolCall || m.capabilities.toolCall) &&\n (!caps.reasoning || m.capabilities.reasoning),\n )\n : models;\n return filtered[0] ?? null;\n}\n\n/**\n * Resolve a per-node model spec to a concrete model id. Precedence: explicit\n * `id` → non-text `modality` (most-available matching model) → `reasoningEffort`\n * tier (text frontier) → text `capabilities` match → `fallback` (the run\n * default). Never throws — degrades to `fallback`.\n */\nexport async function resolveModelSpec(spec: ModelSpecHints, fallback: string): Promise<string> {\n try {\n if (spec.id) return spec.id;\n\n if (spec.modality && spec.modality !== \"text\") {\n const match = pickByCapabilities(\n await listModelsByModality(spec.modality),\n spec.capabilities,\n );\n if (match) return match.offerings[0]?.id ?? match.key;\n return fallback;\n }\n\n if (spec.reasoningEffort) return resolveFrontierModel(EFFORT_TIER[spec.reasoningEffort]);\n\n if (spec.capabilities) {\n const match = pickByCapabilities(await listModelsByModality(\"text\"), spec.capabilities);\n if (match) return match.offerings[0]?.id ?? match.key;\n }\n\n return fallback;\n } catch {\n return fallback;\n }\n}\n\n/**\n * Default OpenAI-compatible base URL per provider bucket (env-overridable).\n * Single home for these — BYOK and any other caller import from here.\n */\nexport function providerBaseUrl(provider: AIProviderId): string | null {\n switch (provider) {\n case \"OPENAI\":\n return process.env.OPENAI_BASE_URL ?? \"https://api.openai.com/v1\";\n case \"ANTHROPIC\":\n return process.env.ANTHROPIC_BASE_URL ?? \"https://api.anthropic.com/v1\";\n case \"GOOGLE\":\n return (\n process.env.GOOGLE_BASE_URL ?? \"https://generativelanguage.googleapis.com/v1beta/openai\"\n );\n case \"SILICONFLOW\":\n return process.env.SILICONFLOW_BASE_URL ?? \"https://api.siliconflow.cn/v1\";\n default:\n // CUSTOM has no canonical endpoint — the caller must supply a baseUrl.\n return null;\n }\n}\n"]}
|