dsh-session-fee 0.0.0-stage → 0.4.4
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/PRICES.md +79 -0
- package/README.md +205 -2
- package/client.js +379 -0
- package/cordis.patch.yml +5 -0
- package/icon.svg +9 -0
- package/index.js +87 -0
- package/lib/catalog.js +228 -0
- package/lib/config.js +68 -0
- package/lib/pricing.js +405 -0
- package/lib/projection.js +295 -0
- package/lib/reload.js +68 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +64 -4
- package/prices.example.json +71 -0
- package/prices.json +213 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// Minimal validators with the `.parse()` DSH's projection registry calls; no
|
|
2
|
+
// runtime dependency. Restored checkpoints that fail them are re-folded.
|
|
3
|
+
const count = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
4
|
+
const amount = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
5
|
+
const text = (value) => typeof value === "string";
|
|
6
|
+
const nullable = (check) => (value) => value === null || check(value);
|
|
7
|
+
const list = (check) => (value) => Array.isArray(value) && value.every(check);
|
|
8
|
+
const shape = (fields) => (value) => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
9
|
+
&& Object.entries(fields).every(([key, check]) => check(value[key]));
|
|
10
|
+
|
|
11
|
+
function schema(name, check) {
|
|
12
|
+
return {
|
|
13
|
+
parse(value) {
|
|
14
|
+
if (!check(value)) throw new TypeError(`Invalid session-fee ${name}`);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const route = shape({ provider: text, model: text });
|
|
21
|
+
const usage = shape({
|
|
22
|
+
uncachedInputTokens: count,
|
|
23
|
+
outputTokens: count,
|
|
24
|
+
cacheReadTokens: count,
|
|
25
|
+
cacheWriteTokens: count,
|
|
26
|
+
reasoningTokens: count
|
|
27
|
+
});
|
|
28
|
+
const attemptShape = shape({
|
|
29
|
+
turn: count,
|
|
30
|
+
step: count,
|
|
31
|
+
firstSeq: count,
|
|
32
|
+
lastSeq: count,
|
|
33
|
+
startTime: amount,
|
|
34
|
+
route: nullable(route),
|
|
35
|
+
usage
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const stateSchema = schema("state", shape({
|
|
39
|
+
inherited: count,
|
|
40
|
+
attempts: list(attemptShape),
|
|
41
|
+
route: nullable(route),
|
|
42
|
+
open: nullable(shape({ turn: count, step: count, startTime: amount })),
|
|
43
|
+
current: nullable(shape({ index: count, turn: count, step: count })),
|
|
44
|
+
compaction: nullable(shape({ turn: count, startTime: amount }))
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const viewSchema = schema("view", shape({
|
|
48
|
+
totalCalls: count,
|
|
49
|
+
pricedCalls: count,
|
|
50
|
+
segments: list(shape({
|
|
51
|
+
provider: text,
|
|
52
|
+
model: text,
|
|
53
|
+
calls: count,
|
|
54
|
+
unknownCalls: count,
|
|
55
|
+
firstTime: amount,
|
|
56
|
+
lastTime: amount,
|
|
57
|
+
usage,
|
|
58
|
+
amounts: list(shape({ currency: text, amount })),
|
|
59
|
+
rules: list(text)
|
|
60
|
+
}))
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
const zeroUsage = () => ({
|
|
64
|
+
uncachedInputTokens: 0,
|
|
65
|
+
outputTokens: 0,
|
|
66
|
+
cacheReadTokens: 0,
|
|
67
|
+
cacheWriteTokens: 0,
|
|
68
|
+
reasoningTokens: 0
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function sameRoute(left, right) {
|
|
72
|
+
return left === right || left !== null && right !== null && left.provider === right.provider && left.model === right.model;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function headerRoute(event) {
|
|
76
|
+
const config = event.data.header?.config;
|
|
77
|
+
if (typeof config?.provider !== "string" || typeof config?.model !== "string" || config.provider === "" || config.model === "") return null;
|
|
78
|
+
return { provider: config.provider, model: config.model };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function messageRoute(event) {
|
|
82
|
+
if (event.type !== "assistant/message") return null;
|
|
83
|
+
const source = event.data.message?.source;
|
|
84
|
+
if (typeof source?.provider !== "string" || typeof source?.model !== "string" || source.provider === "" || source.model === "") return null;
|
|
85
|
+
return { provider: source.provider, model: source.model };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Compaction summaries report their own route; older logs only carry the session route. */
|
|
89
|
+
function summaryRoute(event) {
|
|
90
|
+
const { provider, model } = event.data;
|
|
91
|
+
if (typeof provider !== "string" || typeof model !== "string" || provider === "" || model === "") return null;
|
|
92
|
+
return { provider, model };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function lastUsageChunk(stream) {
|
|
96
|
+
if (!Array.isArray(stream)) return null;
|
|
97
|
+
for (let index = stream.length - 1; index >= 0; index -= 1) {
|
|
98
|
+
const record = stream[index];
|
|
99
|
+
if (record?.type === "chunk" && record.chunk?.type === "usage") return record.chunk.usage;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function usageOf(event) {
|
|
105
|
+
if (event.type === "assistant/message" && event.data.usage !== void 0) return event.data.usage;
|
|
106
|
+
if (event.type !== "assistant/message" && event.type !== "assistant/attempt") return null;
|
|
107
|
+
return lastUsageChunk(event.data.stream);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function bucketsOf(sample) {
|
|
111
|
+
if (typeof sample !== "object" || sample === null || !count(sample.inputTokens) || !count(sample.outputTokens)) return null;
|
|
112
|
+
if (sample.cacheReadTokens !== void 0 && !count(sample.cacheReadTokens)) return null;
|
|
113
|
+
if (sample.cacheWriteTokens !== void 0 && !count(sample.cacheWriteTokens)) return null;
|
|
114
|
+
if (sample.reasoningTokens !== void 0 && !count(sample.reasoningTokens)) return null;
|
|
115
|
+
// DSH usage buckets are disjoint: inputTokens already excludes cache reads/writes.
|
|
116
|
+
return {
|
|
117
|
+
uncachedInputTokens: sample.inputTokens,
|
|
118
|
+
outputTokens: sample.outputTokens,
|
|
119
|
+
cacheReadTokens: sample.cacheReadTokens ?? 0,
|
|
120
|
+
cacheWriteTokens: sample.cacheWriteTokens ?? 0,
|
|
121
|
+
reasoningTokens: sample.reasoningTokens ?? 0
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Build a projection definition closed over one pricer (price book + catalog). */
|
|
126
|
+
export function createSessionCostProjection(pricer) {
|
|
127
|
+
function attempt(event, route, usage, startTime) {
|
|
128
|
+
return {
|
|
129
|
+
turn: event.data.turn,
|
|
130
|
+
step: event.data.step,
|
|
131
|
+
firstSeq: event.seq,
|
|
132
|
+
lastSeq: event.seq,
|
|
133
|
+
startTime,
|
|
134
|
+
route,
|
|
135
|
+
usage
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function applyUsage(state, event) {
|
|
140
|
+
const usage = bucketsOf(usageOf(event));
|
|
141
|
+
if (usage === null || !count(event.data.turn) || !count(event.data.step)) return state;
|
|
142
|
+
const current = state.current;
|
|
143
|
+
const route = messageRoute(event) ?? state.route;
|
|
144
|
+
if (current !== null && current.turn === event.data.turn && current.step === event.data.step) {
|
|
145
|
+
const previous = state.attempts[current.index];
|
|
146
|
+
if (previous === void 0) return state;
|
|
147
|
+
const nextRoute = route ?? previous.route;
|
|
148
|
+
const replacement = {
|
|
149
|
+
...previous,
|
|
150
|
+
lastSeq: event.seq,
|
|
151
|
+
route: nextRoute,
|
|
152
|
+
usage
|
|
153
|
+
};
|
|
154
|
+
const attempts = state.attempts.slice();
|
|
155
|
+
attempts[current.index] = replacement;
|
|
156
|
+
return { ...state, attempts };
|
|
157
|
+
}
|
|
158
|
+
const open = state.open;
|
|
159
|
+
const startTime = open !== null && open.turn === event.data.turn && open.step === event.data.step ? open.startTime : event.time;
|
|
160
|
+
const next = attempt(event, route, usage, startTime);
|
|
161
|
+
return {
|
|
162
|
+
...state,
|
|
163
|
+
attempts: [...state.attempts, next],
|
|
164
|
+
current: { index: state.attempts.length, turn: next.turn, step: next.step }
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Compaction summarisation is a real model call with its own provider-reported
|
|
170
|
+
* usage, billed at the time the summary request started. It is appended as its
|
|
171
|
+
* own attempt (step 0, never `current`) so a later assistant/message for the
|
|
172
|
+
* surrounding step still replaces that step instead of this one.
|
|
173
|
+
*/
|
|
174
|
+
function applyCompaction(state, event) {
|
|
175
|
+
const usage = bucketsOf(event.data.usage);
|
|
176
|
+
if (usage === null) return state;
|
|
177
|
+
const started = state.compaction;
|
|
178
|
+
const next = {
|
|
179
|
+
turn: started?.turn ?? 0,
|
|
180
|
+
step: 0,
|
|
181
|
+
firstSeq: event.seq,
|
|
182
|
+
lastSeq: event.seq,
|
|
183
|
+
startTime: started?.startTime ?? event.time,
|
|
184
|
+
route: summaryRoute(event) ?? state.route,
|
|
185
|
+
usage
|
|
186
|
+
};
|
|
187
|
+
return { ...state, attempts: [...state.attempts, next] };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function view(state) {
|
|
191
|
+
const segments = [];
|
|
192
|
+
let pricedCalls = 0;
|
|
193
|
+
for (const item of state.attempts) {
|
|
194
|
+
const provider = item.route?.provider ?? "";
|
|
195
|
+
const model = item.route?.model ?? "";
|
|
196
|
+
let segment = segments.at(-1);
|
|
197
|
+
if (segment === void 0 || segment.provider !== provider || segment.model !== model) {
|
|
198
|
+
segment = {
|
|
199
|
+
provider,
|
|
200
|
+
model,
|
|
201
|
+
calls: 0,
|
|
202
|
+
unknownCalls: 0,
|
|
203
|
+
firstTime: item.startTime,
|
|
204
|
+
lastTime: item.startTime,
|
|
205
|
+
usage: zeroUsage(),
|
|
206
|
+
amounts: [],
|
|
207
|
+
rules: []
|
|
208
|
+
};
|
|
209
|
+
segments.push(segment);
|
|
210
|
+
}
|
|
211
|
+
segment.calls += 1;
|
|
212
|
+
segment.lastTime = item.startTime;
|
|
213
|
+
for (const key of Object.keys(segment.usage)) segment.usage[key] += item.usage[key];
|
|
214
|
+
// Cost is deliberately derived at the wire boundary. Projection checkpoints
|
|
215
|
+
// contain only log facts, so a new price book reprices the whole history.
|
|
216
|
+
const cost = item.route === null ? null : pricer.price(item.route, item.usage, item.startTime);
|
|
217
|
+
if (cost === null) {
|
|
218
|
+
segment.unknownCalls += 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
pricedCalls += 1;
|
|
222
|
+
let amount = segment.amounts.find((entry) => entry.currency === cost.currency);
|
|
223
|
+
if (amount === void 0) {
|
|
224
|
+
amount = { currency: cost.currency, amount: 0 };
|
|
225
|
+
segment.amounts.push(amount);
|
|
226
|
+
}
|
|
227
|
+
amount.amount += cost.amount;
|
|
228
|
+
if (!segment.rules.includes(cost.rule)) segment.rules.push(cost.rule);
|
|
229
|
+
}
|
|
230
|
+
return { totalCalls: state.attempts.length, pricedCalls, segments };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
key: "sessionCost",
|
|
235
|
+
stateVersion: 6,
|
|
236
|
+
stateSchema,
|
|
237
|
+
// Forks (branches, fork subagents) start with a copy of the parent's log.
|
|
238
|
+
// Those inherited calls were paid in the parent, so they are skipped here;
|
|
239
|
+
// otherwise a parent total that includes its subagents would count them twice.
|
|
240
|
+
init: (_header, inheritedEventCount) => ({
|
|
241
|
+
inherited: Number.isSafeInteger(inheritedEventCount) && inheritedEventCount > 0 ? inheritedEventCount : 0,
|
|
242
|
+
attempts: [],
|
|
243
|
+
route: null,
|
|
244
|
+
open: null,
|
|
245
|
+
current: null,
|
|
246
|
+
compaction: null
|
|
247
|
+
}),
|
|
248
|
+
apply: (state, event) => {
|
|
249
|
+
if (event.seq < state.inherited) return state;
|
|
250
|
+
switch (event.type) {
|
|
251
|
+
case "request/header": {
|
|
252
|
+
const route = headerRoute(event);
|
|
253
|
+
const open = state.open !== null && state.current === null ? { ...state.open, startTime: event.time } : state.open;
|
|
254
|
+
return sameRoute(state.route, route) && open === state.open ? state : { ...state, route, open };
|
|
255
|
+
}
|
|
256
|
+
case "request/context":
|
|
257
|
+
if (state.open === null || state.current !== null) return state;
|
|
258
|
+
return { ...state, open: { ...state.open, startTime: event.time } };
|
|
259
|
+
case "step/start":
|
|
260
|
+
return {
|
|
261
|
+
...state,
|
|
262
|
+
open: { turn: event.data.turn, step: event.data.step, startTime: event.time },
|
|
263
|
+
current: null
|
|
264
|
+
};
|
|
265
|
+
case "llm/retry-started": {
|
|
266
|
+
const matches = state.current?.turn === event.data.turn && state.current?.step === event.data.step || state.open?.turn === event.data.turn && state.open?.step === event.data.step;
|
|
267
|
+
if (!matches) return state;
|
|
268
|
+
return {
|
|
269
|
+
...state,
|
|
270
|
+
open: state.open === null ? null : { ...state.open, startTime: event.time },
|
|
271
|
+
current: null
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
case "step/end":
|
|
275
|
+
if (state.open === null && state.current === null) return state;
|
|
276
|
+
return { ...state, open: null, current: null };
|
|
277
|
+
case "assistant/attempt":
|
|
278
|
+
case "assistant/message":
|
|
279
|
+
return applyUsage(state, event);
|
|
280
|
+
case "compaction/start":
|
|
281
|
+
return {
|
|
282
|
+
...state,
|
|
283
|
+
compaction: { turn: count(event.data.turn) ? event.data.turn : 0, startTime: event.time }
|
|
284
|
+
};
|
|
285
|
+
case "compaction/summary":
|
|
286
|
+
return applyCompaction(state, event);
|
|
287
|
+
case "compaction/end":
|
|
288
|
+
return state.compaction === null ? state : { ...state, compaction: null };
|
|
289
|
+
default:
|
|
290
|
+
return state;
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
wire: { viewSchema, view }
|
|
294
|
+
};
|
|
295
|
+
}
|
package/lib/reload.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { utimesSync, watch } from "node:fs";
|
|
2
|
+
import { basename, dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Keep one valid pricing registration live. Saving the price book (atomically
|
|
6
|
+
* or in place) re-registers the Host projection, then touches the Client
|
|
7
|
+
* artifact so DSH's Client HMR re-reads the current projection snapshot.
|
|
8
|
+
*
|
|
9
|
+
* `load(path)` builds the whole pricing context; `fingerprint(context)` decides
|
|
10
|
+
* whether anything changed. `onLoad` / `onError` report each outcome.
|
|
11
|
+
*/
|
|
12
|
+
export function watchPriceBook({
|
|
13
|
+
priceBookPath,
|
|
14
|
+
clientArtifact,
|
|
15
|
+
load,
|
|
16
|
+
register,
|
|
17
|
+
onError,
|
|
18
|
+
fingerprint = (value) => JSON.stringify(value),
|
|
19
|
+
onLoad = () => {}
|
|
20
|
+
}) {
|
|
21
|
+
let current = load(priceBookPath);
|
|
22
|
+
let dispose = register(current);
|
|
23
|
+
let stopped = false;
|
|
24
|
+
let timer = null;
|
|
25
|
+
onLoad(current);
|
|
26
|
+
|
|
27
|
+
const report = (error) => {
|
|
28
|
+
if (!stopped) onError(error);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const reload = () => {
|
|
32
|
+
timer = null;
|
|
33
|
+
if (stopped) return;
|
|
34
|
+
try {
|
|
35
|
+
const next = load(priceBookPath);
|
|
36
|
+
if (fingerprint(next) === fingerprint(current)) return;
|
|
37
|
+
dispose();
|
|
38
|
+
dispose = register(next);
|
|
39
|
+
current = next;
|
|
40
|
+
onLoad(next);
|
|
41
|
+
// dsh-client-hmr polls artifact mtime/ctime/size. Content and Git stay
|
|
42
|
+
// unchanged, while the Client re-reads the newly registered projection.
|
|
43
|
+
utimesSync(clientArtifact, new Date(), new Date());
|
|
44
|
+
} catch (error) {
|
|
45
|
+
// A half-written or invalid file never displaces the last good book.
|
|
46
|
+
report(error);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const directory = dirname(priceBookPath);
|
|
51
|
+
const filename = basename(priceBookPath);
|
|
52
|
+
const watcher = watch(directory, { persistent: false }, (_event, changed) => {
|
|
53
|
+
if (stopped || changed !== null && changed !== filename) return;
|
|
54
|
+
if (timer !== null) clearTimeout(timer);
|
|
55
|
+
timer = setTimeout(reload, 180);
|
|
56
|
+
});
|
|
57
|
+
watcher.on("error", report);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
close() {
|
|
61
|
+
if (stopped) return;
|
|
62
|
+
stopped = true;
|
|
63
|
+
if (timer !== null) clearTimeout(timer);
|
|
64
|
+
watcher.close();
|
|
65
|
+
dispose();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-fee",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.4.4",
|
|
4
|
+
"description": "在 DSH 输入框下显示当前会话(含子代理)按市面 token 价折算的费用:自维护价格表(别名、峰闲时、节假日、限时、长上下文阶梯)+ pi-ai 模型目录兜底。",
|
|
5
|
+
"author": "youxia",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/fujingzhai/youxia-dsh-plugins.git",
|
|
9
|
+
"directory": "dsh-session-fee"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/fujingzhai/youxia-dsh-plugins/tree/main/dsh-session-fee#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/fujingzhai/youxia-dsh-plugins/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./index.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./index.js",
|
|
19
|
+
"./client": "./client.js",
|
|
20
|
+
"./locale/*.json": "./locale/*.json",
|
|
21
|
+
"./icon": "./icon.svg",
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"index.js",
|
|
26
|
+
"client.js",
|
|
27
|
+
"lib/*.js",
|
|
28
|
+
"cordis.patch.yml",
|
|
29
|
+
"prices.json",
|
|
30
|
+
"prices.example.json",
|
|
31
|
+
"PRICES.md",
|
|
32
|
+
"locale/*.json",
|
|
33
|
+
"icon.svg",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"dsh": {
|
|
38
|
+
"bundle": {
|
|
39
|
+
"patch": "./cordis.patch.yml"
|
|
40
|
+
},
|
|
41
|
+
"client": {
|
|
42
|
+
"platform": "web",
|
|
43
|
+
"immediately": true,
|
|
44
|
+
"inject": [
|
|
45
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"check": "node --check index.js && node --check client.js && node --check lib/pricing.js && node --check lib/catalog.js && node --check lib/config.js && node --check lib/projection.js && node --check lib/reload.js",
|
|
52
|
+
"test": "node --test"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=24.9",
|
|
56
|
+
"dsh": ">=0.2.1-rc.5"
|
|
57
|
+
},
|
|
58
|
+
"keywords": [
|
|
59
|
+
"dsh-plugin",
|
|
60
|
+
"deepseek-harness",
|
|
61
|
+
"token",
|
|
62
|
+
"cost",
|
|
63
|
+
"pricing"
|
|
64
|
+
],
|
|
65
|
+
"license": "MIT"
|
|
66
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"note": "写法示例,金额均为虚构,不是实际报价。",
|
|
4
|
+
"timeZone": "Asia/Shanghai",
|
|
5
|
+
"dateSets": {
|
|
6
|
+
"holidays": ["2026-10-01..2026-10-07", "2026-12-25"]
|
|
7
|
+
},
|
|
8
|
+
"models": [
|
|
9
|
+
{
|
|
10
|
+
"name": "示例国产模型(多个路由共用一份价格)",
|
|
11
|
+
"match": [
|
|
12
|
+
"your-provider/internal-key",
|
|
13
|
+
"another-relay/example-chat",
|
|
14
|
+
"*/example-chat"
|
|
15
|
+
],
|
|
16
|
+
"rules": [
|
|
17
|
+
{
|
|
18
|
+
"name": "工作日高峰(虚构)",
|
|
19
|
+
"currency": "CNY",
|
|
20
|
+
"input": 4,
|
|
21
|
+
"cacheRead": 0.4,
|
|
22
|
+
"cacheWrite": 0,
|
|
23
|
+
"output": 8,
|
|
24
|
+
"when": {
|
|
25
|
+
"weekdays": [1, 2, 3, 4, 5],
|
|
26
|
+
"excludeDates": ["@holidays"],
|
|
27
|
+
"intervals": [{ "start": "09:00", "end": "12:00" }, { "start": "14:00", "end": "18:00" }]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "每日深夜(虚构,跨午夜)",
|
|
32
|
+
"currency": "CNY",
|
|
33
|
+
"input": 1,
|
|
34
|
+
"cacheRead": 0.1,
|
|
35
|
+
"cacheWrite": 0,
|
|
36
|
+
"output": 2,
|
|
37
|
+
"when": { "intervals": [{ "start": "22:00", "end": "08:00" }] }
|
|
38
|
+
},
|
|
39
|
+
{ "name": "常规(虚构)", "currency": "CNY", "input": 2, "cacheRead": 0.2, "cacheWrite": 0, "output": 4 }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "示例美国模型(限时价+长上下文阶梯)",
|
|
44
|
+
"match": ["*/example-pro*"],
|
|
45
|
+
"rules": [
|
|
46
|
+
{
|
|
47
|
+
"name": "首发限时(虚构)",
|
|
48
|
+
"currency": "USD",
|
|
49
|
+
"input": 1,
|
|
50
|
+
"cacheRead": 0.1,
|
|
51
|
+
"output": 5,
|
|
52
|
+
"when": { "to": "2026-12-01T00:00:00+08:00" },
|
|
53
|
+
"tiers": [{ "above": 200000, "input": 2, "cacheRead": 0.2, "output": 7.5 }]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "标准(虚构)",
|
|
57
|
+
"currency": "USD",
|
|
58
|
+
"input": 2,
|
|
59
|
+
"cacheRead": 0.2,
|
|
60
|
+
"output": 10,
|
|
61
|
+
"tiers": [{ "above": 200000, "input": 4, "cacheRead": 0.4, "output": 15 }]
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "怪名字的路由借用 pi-ai 目录里的价格",
|
|
67
|
+
"match": ["antigravity/gemini-3.1-pro-high"],
|
|
68
|
+
"catalog": "google/gemini-3.1-pro-preview"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|