pjpjq commited on
Commit
303ab39
·
1 Parent(s): 1009c14

feat: bootstrap model prices on management page

Browse files
Files changed (3) hide show
  1. Dockerfile +1 -0
  2. entrypoint.sh +17 -0
  3. model_price_bootstrap.py +232 -0
Dockerfile CHANGED
@@ -21,6 +21,7 @@ ENV USAGE_AUTOSAVE_INTERVAL=300
21
 
22
  COPY entrypoint.sh /opt/daili/entrypoint.sh
23
  COPY hf_snapshot.py /opt/daili/hf_snapshot.py
 
24
  RUN chmod +x /opt/daili/entrypoint.sh
25
 
26
  EXPOSE 8317
 
21
 
22
  COPY entrypoint.sh /opt/daili/entrypoint.sh
23
  COPY hf_snapshot.py /opt/daili/hf_snapshot.py
24
+ COPY model_price_bootstrap.py /opt/daili/model_price_bootstrap.py
25
  RUN chmod +x /opt/daili/entrypoint.sh
26
 
27
  EXPOSE 8317
entrypoint.sh CHANGED
@@ -26,6 +26,10 @@ MC_CONFIG_DIR="${MC_CONFIG_DIR:-${AUTH_BASE}/.mc}"
26
  USAGE_HF_TOKEN="${USAGE_HF_TOKEN:-${HF_TOKEN:-}}"
27
  USAGE_HF_REPO_ID="${USAGE_HF_REPO_ID:-pjpjq/daili-usage-state}"
28
  USAGE_HF_PATH="${USAGE_HF_PATH:-usage-export.json}"
 
 
 
 
29
  CONFIG_PATH="/opt/daili/config.yaml"
30
  GATEWAY_BIN="/usr/local/bin/daili-gateway"
31
  GATEWAY_PID=""
@@ -238,6 +242,16 @@ backup_usage() {
238
  fi
239
  }
240
 
 
 
 
 
 
 
 
 
 
 
241
  restore_usage() {
242
  download_r2_snapshot
243
  download_hf_snapshot
@@ -259,6 +273,7 @@ start_autosave_loop() {
259
  (
260
  while true; do
261
  sleep "$USAGE_AUTOSAVE_INTERVAL"
 
262
  backup_usage
263
  done
264
  ) &
@@ -289,10 +304,12 @@ unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
289
  GATEWAY_PID="$!"
290
 
291
  restore_usage || true
 
292
  start_autosave_loop
293
 
294
  wait "$GATEWAY_PID"
295
  status="$?"
296
  stop_background_jobs
 
297
  backup_usage
298
  exit "$status"
 
26
  USAGE_HF_TOKEN="${USAGE_HF_TOKEN:-${HF_TOKEN:-}}"
27
  USAGE_HF_REPO_ID="${USAGE_HF_REPO_ID:-pjpjq/daili-usage-state}"
28
  USAGE_HF_PATH="${USAGE_HF_PATH:-usage-export.json}"
29
+ MODEL_PRICES_SOURCE_URL="${MODEL_PRICES_SOURCE_URL:-https://zhanzhong.zeabur.app/api/pricing}"
30
+ MODEL_PRICES_OUTPUT_PATH="${MODEL_PRICES_OUTPUT_PATH:-${AUTH_BASE}/usage-state/model-prices.json}"
31
+ MODEL_PRICES_FETCH_TIMEOUT="${MODEL_PRICES_FETCH_TIMEOUT:-20}"
32
+ MANAGEMENT_HTML_PATH="${MANAGEMENT_HTML_PATH:-${AUTH_BASE}/static/management.html}"
33
  CONFIG_PATH="/opt/daili/config.yaml"
34
  GATEWAY_BIN="/usr/local/bin/daili-gateway"
35
  GATEWAY_PID=""
 
242
  fi
243
  }
244
 
245
+ refresh_model_prices() {
246
+ env \
247
+ MODEL_PRICES_SOURCE_URL="$MODEL_PRICES_SOURCE_URL" \
248
+ MODEL_PRICES_OUTPUT_PATH="$MODEL_PRICES_OUTPUT_PATH" \
249
+ MODEL_PRICES_FETCH_TIMEOUT="$MODEL_PRICES_FETCH_TIMEOUT" \
250
+ MANAGEMENT_HTML_PATH="$MANAGEMENT_HTML_PATH" \
251
+ WRITABLE_PATH="$AUTH_BASE" \
252
+ /usr/local/bin/python3 /opt/daili/model_price_bootstrap.py >/dev/null 2>&1 || true
253
+ }
254
+
255
  restore_usage() {
256
  download_r2_snapshot
257
  download_hf_snapshot
 
273
  (
274
  while true; do
275
  sleep "$USAGE_AUTOSAVE_INTERVAL"
276
+ refresh_model_prices
277
  backup_usage
278
  done
279
  ) &
 
304
  GATEWAY_PID="$!"
305
 
306
  restore_usage || true
307
+ refresh_model_prices
308
  start_autosave_loop
309
 
310
  wait "$GATEWAY_PID"
311
  status="$?"
312
  stop_background_jobs
313
+ refresh_model_prices
314
  backup_usage
315
  exit "$status"
model_price_bootstrap.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from urllib.error import URLError
10
+ from urllib.request import urlopen
11
+
12
+
13
+ SCRIPT_START = "<!-- model-price-bootstrap:start -->"
14
+ SCRIPT_END = "<!-- model-price-bootstrap:end -->"
15
+ LOCAL_STORAGE_KEY = "cli-proxy-model-prices-v2"
16
+ DEFAULT_SOURCE_URL = "https://zhanzhong.zeabur.app/api/pricing"
17
+ DEFAULT_TIMEOUT_SECONDS = 20
18
+
19
+
20
+ def env_path(name: str, default: str) -> Path:
21
+ value = os.environ.get(name, "").strip() or default
22
+ return Path(value)
23
+
24
+
25
+ def env_text(name: str, default: str) -> str:
26
+ return os.environ.get(name, "").strip() or default
27
+
28
+
29
+ def env_int(name: str, default: int) -> int:
30
+ raw = os.environ.get(name, "").strip()
31
+ if not raw:
32
+ return default
33
+ try:
34
+ return max(1, int(raw))
35
+ except ValueError:
36
+ return default
37
+
38
+
39
+ def atomic_write(path: Path, content: str) -> None:
40
+ path.parent.mkdir(parents=True, exist_ok=True)
41
+ with tempfile.NamedTemporaryFile(
42
+ "w",
43
+ encoding="utf-8",
44
+ delete=False,
45
+ dir=str(path.parent),
46
+ ) as tmp:
47
+ tmp.write(content)
48
+ tmp_path = Path(tmp.name)
49
+ tmp_path.replace(path)
50
+
51
+
52
+ def load_json(path: Path) -> dict[str, Any]:
53
+ if not path.exists():
54
+ return {}
55
+ try:
56
+ return json.loads(path.read_text(encoding="utf-8"))
57
+ except (OSError, json.JSONDecodeError):
58
+ return {}
59
+
60
+
61
+ def fetch_remote_json(url: str, timeout: int) -> dict[str, Any]:
62
+ with urlopen(url, timeout=timeout) as response:
63
+ data = json.load(response)
64
+ if not isinstance(data, dict):
65
+ raise ValueError("remote pricing payload is not a JSON object")
66
+ return data
67
+
68
+
69
+ def normalize_price(value: Any) -> float:
70
+ try:
71
+ return round(float(value), 6)
72
+ except (TypeError, ValueError):
73
+ return 0.0
74
+
75
+
76
+ def build_prices_from_newapi(payload: dict[str, Any]) -> dict[str, dict[str, float]]:
77
+ rows = payload.get("data")
78
+ if not isinstance(rows, list):
79
+ raise ValueError("pricing payload missing data list")
80
+
81
+ group_ratio = payload.get("group_ratio")
82
+ default_group_ratio = 1.0
83
+ if isinstance(group_ratio, dict):
84
+ default_group_ratio = float(group_ratio.get("default", 1) or 1)
85
+
86
+ result: dict[str, dict[str, float]] = {}
87
+ for item in rows:
88
+ if not isinstance(item, dict):
89
+ continue
90
+ if item.get("quota_type") != 0:
91
+ continue
92
+
93
+ model_name = str(item.get("model_name", "")).strip()
94
+ if not model_name:
95
+ continue
96
+
97
+ prompt = normalize_price(item.get("model_ratio", 0) * 2 * default_group_ratio)
98
+ if prompt <= 0:
99
+ continue
100
+
101
+ completion_ratio = normalize_price(item.get("completion_ratio", 1))
102
+ completion = normalize_price(prompt * completion_ratio)
103
+
104
+ cache_ratio = item.get("cache_ratio")
105
+ cache = prompt if cache_ratio is None else normalize_price(prompt * cache_ratio)
106
+
107
+ result[model_name] = {
108
+ "prompt": prompt,
109
+ "completion": completion,
110
+ "cache": cache,
111
+ }
112
+
113
+ if not result:
114
+ raise ValueError("no per-token pricing models found")
115
+
116
+ return result
117
+
118
+
119
+ def build_script(prices: dict[str, dict[str, float]]) -> str:
120
+ serialized = json.dumps(prices, ensure_ascii=False, separators=(",", ":"))
121
+ return f"""{SCRIPT_START}
122
+ <script>
123
+ (function () {{
124
+ const STORAGE_KEY = {json.dumps(LOCAL_STORAGE_KEY)};
125
+ const DEFAULT_PRICES = {serialized};
126
+
127
+ const normalize = (value) => {{
128
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
129
+ const prompt = Number(value.prompt);
130
+ const completion = Number(value.completion);
131
+ const cache = Number(value.cache);
132
+ if (!Number.isFinite(prompt) || !Number.isFinite(completion) || !Number.isFinite(cache)) {{
133
+ return null;
134
+ }}
135
+ return {{ prompt, completion, cache }};
136
+ }};
137
+
138
+ const sanitizeMap = (value) => {{
139
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {{}};
140
+ const next = {{}};
141
+ Object.entries(value).forEach(([key, item]) => {{
142
+ const normalized = normalize(item);
143
+ if (normalized) {{
144
+ next[String(key)] = normalized;
145
+ }}
146
+ }});
147
+ return next;
148
+ }};
149
+
150
+ const applyDefaults = () => {{
151
+ let current = {{}};
152
+ try {{
153
+ current = sanitizeMap(JSON.parse(localStorage.getItem(STORAGE_KEY) || '{{}}'));
154
+ }} catch (_error) {{
155
+ current = {{}};
156
+ }}
157
+
158
+ const merged = {{ ...DEFAULT_PRICES, ...current }};
159
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(merged));
160
+ }};
161
+
162
+ if (document.readyState === 'loading') {{
163
+ document.addEventListener('DOMContentLoaded', applyDefaults, {{ once: true }});
164
+ }} else {{
165
+ applyDefaults();
166
+ }}
167
+ }})();
168
+ </script>
169
+ {SCRIPT_END}"""
170
+
171
+
172
+ def patch_management_html(html_path: Path, prices: dict[str, dict[str, float]]) -> bool:
173
+ if not html_path.exists():
174
+ return False
175
+
176
+ html = html_path.read_text(encoding="utf-8")
177
+ script_block = build_script(prices)
178
+ start_index = html.find(SCRIPT_START)
179
+ end_index = html.find(SCRIPT_END)
180
+
181
+ if start_index != -1 and end_index != -1 and end_index > start_index:
182
+ end_index += len(SCRIPT_END)
183
+ patched = html[:start_index] + script_block + html[end_index:]
184
+ elif "</body>" in html:
185
+ patched = html.replace("</body>", script_block + "\n</body>", 1)
186
+ else:
187
+ patched = html + "\n" + script_block
188
+
189
+ if patched == html:
190
+ return True
191
+
192
+ atomic_write(html_path, patched)
193
+ return True
194
+
195
+
196
+ def main() -> int:
197
+ writable_base = env_text("WRITABLE_PATH", "/tmp")
198
+ html_path = env_path("MANAGEMENT_HTML_PATH", f"{writable_base}/static/management.html")
199
+ prices_path = env_path("MODEL_PRICES_OUTPUT_PATH", f"{writable_base}/usage-state/model-prices.json")
200
+ source_url = env_text("MODEL_PRICES_SOURCE_URL", DEFAULT_SOURCE_URL)
201
+ timeout = env_int("MODEL_PRICES_FETCH_TIMEOUT", DEFAULT_TIMEOUT_SECONDS)
202
+
203
+ prices: dict[str, dict[str, float]] = {}
204
+ try:
205
+ payload = fetch_remote_json(source_url, timeout)
206
+ prices = build_prices_from_newapi(payload)
207
+ atomic_write(prices_path, json.dumps(prices, ensure_ascii=False, indent=2, sort_keys=True))
208
+ except (URLError, TimeoutError, ValueError, OSError, json.JSONDecodeError) as exc:
209
+ cached = load_json(prices_path)
210
+ if isinstance(cached, dict) and cached:
211
+ prices = cached
212
+ print(f"model_price_bootstrap: using cached prices after fetch failure: {exc}", file=sys.stderr)
213
+ else:
214
+ print(f"model_price_bootstrap: unable to fetch or load prices: {exc}", file=sys.stderr)
215
+ return 0
216
+
217
+ if not patch_management_html(html_path, prices):
218
+ print(
219
+ f"model_price_bootstrap: management html not found yet at {html_path}",
220
+ file=sys.stderr,
221
+ )
222
+ return 0
223
+
224
+ print(
225
+ f"model_price_bootstrap: patched {html_path} with {len(prices)} model prices from {source_url}",
226
+ file=sys.stderr,
227
+ )
228
+ return 0
229
+
230
+
231
+ if __name__ == "__main__":
232
+ raise SystemExit(main())