cabletidy 0.1.0 → 0.3.0-rc.2

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.
@@ -1,187 +0,0 @@
1
- export class ModelResolveError extends Error {
2
- constructor(code, message, details = {}) {
3
- super(message);
4
- this.name = "ModelResolveError";
5
- this.code = code;
6
- this.details = details;
7
- }
8
- }
9
-
10
- export function findModelProfile(config, clientModelId, allowedModels) {
11
- if (!clientModelId) return null;
12
- const allowed = Array.isArray(allowedModels) ? new Set(allowedModels) : null;
13
- if (config.models?.[clientModelId] && (!allowed || allowed.has(clientModelId))) {
14
- return { profileId: clientModelId, profile: config.models[clientModelId], matchedBy: "id" };
15
- }
16
- for (const [profileId, profile] of Object.entries(config.models || {})) {
17
- if (allowed && !allowed.has(profileId)) continue;
18
- if (
19
- profile.clientModelId === clientModelId ||
20
- (profile.aliases || []).includes(clientModelId)
21
- ) {
22
- return { profileId, profile, matchedBy: "alias" };
23
- }
24
- }
25
- return null;
26
- }
27
-
28
- export function clientModelIdForProfile(profileId, profile) {
29
- return profile?.clientModelId || profile?.aliases?.[0] || profileId;
30
- }
31
-
32
- export function listClientModels(config, virtualProvider) {
33
- const allowed = virtualProvider?.allowedModels || [];
34
- return allowed
35
- .map((profileId) => {
36
- const profile = config.models?.[profileId];
37
- if (!profile) return null;
38
- return {
39
- id: clientModelIdForProfile(profileId, profile),
40
- profileId,
41
- aliases: profile.aliases || [],
42
- family: profile.family || "unknown",
43
- capabilities: profile.capabilities || [],
44
- object: "model",
45
- owned_by: "cabletidy",
46
- };
47
- })
48
- .filter(Boolean);
49
- }
50
-
51
- export function resolveModelProfile(config, virtualProvider, requestedModel) {
52
- if (requestedModel !== undefined && (typeof requestedModel !== "string" || !requestedModel.trim())) {
53
- throw new ModelResolveError("invalid_model", "model 必须是非空字符串");
54
- }
55
- const clientModelId =
56
- requestedModel ||
57
- virtualProvider?.defaultModel ||
58
- null;
59
- if (!clientModelId) {
60
- throw new ModelResolveError("model_required", "请求没有模型,Virtual Provider 也没有默认模型");
61
- }
62
-
63
- // allowedModels scopes optional overrides, not the models a client may request.
64
- const match = findModelProfile(config, clientModelId, virtualProvider?.allowedModels || []);
65
- if (!match) {
66
- return { clientModelId, profileId: null, profile: null, matchedBy: "passthrough" };
67
- }
68
-
69
- return {
70
- clientModelId: requestedModel || clientModelIdForProfile(match.profileId, match.profile),
71
- profileId: match.profileId,
72
- profile: match.profile,
73
- matchedBy: match.matchedBy,
74
- };
75
- }
76
-
77
- export function effectiveCapabilities(profile, binding) {
78
- const capabilities = new Set(profile?.capabilities || []);
79
- for (const item of binding?.capabilityOverrides || []) {
80
- if (item.startsWith("-")) capabilities.delete(item.slice(1));
81
- else if (item.startsWith("+")) capabilities.add(item.slice(1));
82
- else capabilities.add(item);
83
- }
84
- if (profile?.codex?.metadataMode === "override" &&
85
- profile.codex.inputModalities && !profile.codex.inputModalities.includes("image")) {
86
- capabilities.delete("vision");
87
- }
88
- return capabilities;
89
- }
90
-
91
- function requiredCapabilities(request = {}) {
92
- const required = new Set();
93
- if (request.stream) required.add("streaming");
94
- if (Array.isArray(request.tools) && request.tools.length) required.add("tools");
95
- if (request.tool_choice) required.add("tools");
96
- if (request.parallel_tool_calls) required.add("parallel_tool_calls");
97
- if (request.reasoning) required.add("reasoning");
98
- if (request.thinking && request.thinking.type !== "disabled") required.add("reasoning");
99
- if (request.images || request.image_input || containsImage(request.input) || containsImage(request.messages)) required.add("vision");
100
- return required;
101
- }
102
-
103
- function containsImage(value) {
104
- if (Array.isArray(value)) return value.some(containsImage);
105
- if (!value || typeof value !== "object") return false;
106
- if (["input_image", "image", "image_url"].includes(value.type)) return true;
107
- return containsImage(value.content);
108
- }
109
-
110
- function hasCapabilities(capabilities, required) {
111
- return [...required].every((item) => capabilities.has(item));
112
- }
113
-
114
- export function selectBackend(config, virtualProvider, modelResolution, request = {}) {
115
- const route = config.routes?.[virtualProvider.route];
116
- if (!route) {
117
- throw new ModelResolveError("route_not_found", `Route 不存在: ${virtualProvider.route}`);
118
- }
119
- if (!Array.isArray(route.backends) || route.backends.length !== 1) {
120
- throw new ModelResolveError("invalid_route", "每份配置必须且只能连接一个 upstream");
121
- }
122
-
123
- const backend = route.backends[0];
124
- const upstream = config.upstreams?.[backend?.upstream];
125
- const profile = modelResolution.profile;
126
- const modelBindings = profile?.upstreams || {};
127
- if (Object.keys(modelBindings).length > 1) {
128
- throw new ModelResolveError("invalid_model_binding", "每个 Model Profile 只能映射一个 upstream");
129
- }
130
- const modelBinding = modelBindings[backend?.upstream];
131
- const required = requiredCapabilities(request);
132
- const capabilities = effectiveCapabilities(profile, modelBinding);
133
- let reason;
134
- if (!backend || backend.enabled === false || !upstream || upstream.enabled === false) {
135
- reason = "upstream_disabled_or_missing";
136
- } else if (profile && !backend.models?.includes(modelResolution.profileId)) {
137
- reason = "model_not_in_route";
138
- } else if (profile && !modelBinding) {
139
- reason = "model_binding_missing";
140
- } else if (virtualProvider.ingressProtocol !== upstream.protocol) {
141
- reason = "protocol_transform_missing";
142
- } else if (profile && !hasCapabilities(capabilities, required)) {
143
- reason = "capability_missing";
144
- }
145
- if (reason) {
146
- throw new ModelResolveError(
147
- "no_compatible_upstream",
148
- `当前配置的 upstream 无法处理模型 ${modelResolution.clientModelId}`,
149
- {
150
- profileId: modelResolution.profileId,
151
- rejected: [{ index: 0, reason, ...(reason === "capability_missing"
152
- ? { missing: [...required].filter((item) => !capabilities.has(item)) } : {}) }],
153
- },
154
- );
155
- }
156
- return {
157
- routeId: virtualProvider.route,
158
- backend,
159
- backendIndex: 0,
160
- upstream,
161
- upstreamModelId: modelBinding?.upstreamModelId || modelResolution.clientModelId,
162
- capabilities: [...capabilities],
163
- rejected: [],
164
- };
165
- }
166
-
167
- export function resolveRequest(config, virtualProvider, requestBody = {}) {
168
- const model = resolveModelProfile(config, virtualProvider, requestBody.model);
169
- const backend = selectBackend(config, virtualProvider, model, requestBody);
170
- return { model, ...backend };
171
- }
172
-
173
- export function rewriteModelFields(value, clientModelId, upstreamModelId) {
174
- if (Array.isArray(value)) {
175
- return value.map((item) => rewriteModelFields(item, clientModelId, upstreamModelId));
176
- }
177
- if (!value || typeof value !== "object") return value;
178
- const output = {};
179
- for (const [key, item] of Object.entries(value)) {
180
- if (key === "model" && item === upstreamModelId) {
181
- output[key] = clientModelId;
182
- } else {
183
- output[key] = rewriteModelFields(item, clientModelId, upstreamModelId);
184
- }
185
- }
186
- return output;
187
- }