@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
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/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -1,181 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM 官方定价漂移监测(issue #49 长效机制)。
|
|
3
|
+
*
|
|
4
|
+
* 设计纪律(与 ADR-11 / issue #49 一致):
|
|
5
|
+
* - 永不自动 apply 价格变更到 llm-price-table.json——价格调整必须走 PR,
|
|
6
|
+
* 由人 review;founder 2026-08-26 实测「自动 apply 容易把 down 5% 的官方新价错认为我的 bug」;
|
|
7
|
+
* - 本脚本只产出 PR-就绪的 Markdown diff,供人粘贴到 PR 描述;
|
|
8
|
+
* - 联网取数(--fetch 模式)有超时 + 离线 SKIP,默认行为零网络,适配 CI 纪律;
|
|
9
|
+
* - 解析规则保守:任何一行解析失败 → 整家 SKIP + warn,不写未知数据;
|
|
10
|
+
* - 比较口径:与 fixture 缓存的上次官方价对照——若 fixture 不存在,fetch 模式写入新 fixture,
|
|
11
|
+
* diff 模式直接报"首次运行,无 baseline"。
|
|
12
|
+
*
|
|
13
|
+
* 运行(在 ts/ 下):
|
|
14
|
+
* npx tsx scripts/price-drift-watch.ts # 离线模式:对照 fixture 算 diff(零网络)
|
|
15
|
+
* npx tsx scripts/price-drift-watch.ts --fetch # 联网模式:fetch 官方页 + 写 fixture + 算 diff
|
|
16
|
+
* npx tsx scripts/price-drift-watch.ts --format json
|
|
17
|
+
*
|
|
18
|
+
* 输出:stdout Markdown(默认)/JSON(--format json),心形:
|
|
19
|
+
* {
|
|
20
|
+
* schema_version: 'gotry_llm_price_drift_v1',
|
|
21
|
+
* observed_at: '<ISO>',
|
|
22
|
+
* providers: {
|
|
23
|
+
* deepseek: { status: 'unchanged' | 'drift' | 'skipped_no_baseline' | 'skipped_parse_error' | 'skipped_no_network', drift: [...] },
|
|
24
|
+
* minimax: { ... },
|
|
25
|
+
* openai: { ... },
|
|
26
|
+
* anthropic: { ... }
|
|
27
|
+
* },
|
|
28
|
+
* suggested_pr_paragraph: '<可直接粘贴到 PR 描述的一段>'
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* 心智模型:
|
|
32
|
+
* - provider="deepseek": fixture 文件 data/llm-price-baseline-deepseek.json,
|
|
33
|
+
* 上次 fetch 的官方价(rows 数组 + as_of);若不存在 → "skipped_no_baseline",
|
|
34
|
+
* 提示:首次跑 --fetch 一次入 fixture。
|
|
35
|
+
* - provider="minimax": 同上,data/llm-price-baseline-minimax.json。
|
|
36
|
+
* - provider="openai": 同上,data/llm-price-baseline-openai.json。
|
|
37
|
+
* - provider="anthropic": 同上,data/llm-price-baseline-anthropic.json。
|
|
38
|
+
* - 已入价表的 provider(当前为 deepseek/minimax)才有 baseline 期望;
|
|
39
|
+
* 未入价表的 provider(openai/anthropic)fetch 模式采得就当 narrative 记录,
|
|
40
|
+
* 仍不进 llm-price-table.json(那是未来单独的 PR)。
|
|
41
|
+
*/
|
|
1
42
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
43
|
const FETCH_TIMEOUT_MS = 8000;
|
|
4
44
|
const DEFAULT_PROVIDERS = [
|
|
5
|
-
{
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
parse: parseDeepseekHtml
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
id: 'minimax',
|
|
13
|
-
source_url: 'https://platform.minimax.io/docs/guides/pricing-paygo',
|
|
14
|
-
baseline_path: 'data/llm-price-baseline-minimax.json',
|
|
15
|
-
parse: parseMinimaxHtml
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
id: 'openai',
|
|
19
|
-
source_url: 'https://openai.com/api/pricing/',
|
|
20
|
-
baseline_path: 'data/llm-price-baseline-openai.json',
|
|
21
|
-
snapshot_path: 'data/llm-pricing-snapshot-openai.html',
|
|
22
|
-
parse: parseOpenaiHtml
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
id: 'anthropic',
|
|
26
|
-
source_url: 'https://www.anthropic.com/pricing',
|
|
27
|
-
baseline_path: 'data/llm-price-baseline-anthropic.json',
|
|
28
|
-
snapshot_path: 'data/llm-pricing-snapshot-anthropic.html',
|
|
29
|
-
parse: parseAnthropicHtml
|
|
30
|
-
}
|
|
45
|
+
{ id: 'deepseek', source_url: 'https://api-docs.deepseek.com/quick_start/pricing', baseline_path: 'data/llm-price-baseline-deepseek.json', parse: parseDeepseekHtml },
|
|
46
|
+
{ id: 'minimax', source_url: 'https://platform.minimax.io/docs/guides/pricing-paygo', baseline_path: 'data/llm-price-baseline-minimax.json', parse: parseMinimaxHtml },
|
|
47
|
+
{ id: 'openai', source_url: 'https://openai.com/api/pricing/', baseline_path: 'data/llm-price-baseline-openai.json', snapshot_path: 'data/llm-pricing-snapshot-openai.html', parse: parseOpenaiHtml },
|
|
48
|
+
{ id: 'anthropic', source_url: 'https://www.anthropic.com/pricing', baseline_path: 'data/llm-price-baseline-anthropic.json', snapshot_path: 'data/llm-pricing-snapshot-anthropic.html', parse: parseAnthropicHtml },
|
|
31
49
|
];
|
|
32
50
|
async function fetchWithTimeout(url, timeoutMs) {
|
|
33
51
|
const controller = new AbortController();
|
|
34
|
-
const timer = setTimeout(()=>controller.abort(), timeoutMs);
|
|
52
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
35
53
|
try {
|
|
36
|
-
const res = await fetch(url, {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'User-Agent': 'GoTry-price-drift-watch/1 (issue #49)'
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
54
|
+
const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'GoTry-price-drift-watch/1 (issue #49)' } });
|
|
55
|
+
if (!res.ok)
|
|
56
|
+
throw new Error(`HTTP ${res.status}`);
|
|
43
57
|
return await res.text();
|
|
44
|
-
}
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
45
60
|
clearTimeout(timer);
|
|
46
61
|
}
|
|
47
62
|
}
|
|
63
|
+
/** DeepSeek 官方定价页是简单 HTML 表格;粗解析:美元数字 $/M token 表行。
|
|
64
|
+
* 失败时不抛:返回 null,watcher 标 skipped_parse_error。 */
|
|
48
65
|
function parseDeepseekHtml(html) {
|
|
66
|
+
// DeepSeek 表格行形如:
|
|
67
|
+
// <td>deepseek-v4-flash</td><td>0.44</td><td>0.014</td><td>1.32</td> (cache_miss/cache_hit/output per 1M)
|
|
49
68
|
const rows = [];
|
|
50
69
|
const rowRe = /<tr[^>]*>([\s\S]*?)<\/tr>/g;
|
|
51
70
|
const cellRe = /<td[^>]*>([\s\S]*?)<\/td>/g;
|
|
52
71
|
let tr;
|
|
53
|
-
while((tr = rowRe.exec(html)) !== null){
|
|
72
|
+
while ((tr = rowRe.exec(html)) !== null) {
|
|
54
73
|
const cells = [];
|
|
55
74
|
let m;
|
|
56
|
-
while((m = cellRe.exec(tr[1])) !== null)
|
|
57
|
-
|
|
75
|
+
while ((m = cellRe.exec(tr[1])) !== null)
|
|
76
|
+
cells.push(m[1].replace(/<[^>]+>/g, '').trim());
|
|
77
|
+
if (cells.length < 4)
|
|
78
|
+
continue;
|
|
58
79
|
const model = cells[0];
|
|
59
|
-
if (!/^deepseek-v[34]/.test(model))
|
|
80
|
+
if (!/^deepseek-v[34]/.test(model))
|
|
81
|
+
continue;
|
|
60
82
|
const miss = Number(cells[1]);
|
|
61
83
|
const hit = Number(cells[2]);
|
|
62
84
|
const out = Number(cells[3]);
|
|
63
|
-
if (!Number.isFinite(miss) || !Number.isFinite(out))
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
input_cache_miss_usd_per_1m: miss,
|
|
67
|
-
input_cache_hit_usd_per_1m: Number.isFinite(hit) ? hit : null,
|
|
68
|
-
output_usd_per_1m: out
|
|
69
|
-
});
|
|
85
|
+
if (!Number.isFinite(miss) || !Number.isFinite(out))
|
|
86
|
+
continue;
|
|
87
|
+
rows.push({ model, input_cache_miss_usd_per_1m: miss, input_cache_hit_usd_per_1m: Number.isFinite(hit) ? hit : null, output_usd_per_1m: out });
|
|
70
88
|
}
|
|
71
89
|
return rows.length > 0 ? rows : null;
|
|
72
90
|
}
|
|
91
|
+
/** MiniMax Pay-as-you-go 页面是 markdown 化 HTML 文档,粗解析:model + $/M tokens 行。 */
|
|
73
92
|
function parseMinimaxHtml(html) {
|
|
74
93
|
const rows = [];
|
|
94
|
+
// 行形如: **MiniMax-M3** … $0.30 / M tokens (Input, ≤512k) … $1.20 / M tokens (Output, ≤512k) … $0.06 / M tokens (Cache Read)
|
|
75
95
|
const lineRe = /<p[^>]*>([\s\S]*?)<\/p>/g;
|
|
76
96
|
let p;
|
|
77
97
|
const seen = new Set();
|
|
78
|
-
while((p = lineRe.exec(html)) !== null){
|
|
98
|
+
while ((p = lineRe.exec(html)) !== null) {
|
|
79
99
|
const text = p[1].replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/\s+/g, ' ').trim();
|
|
80
100
|
const modelMatch = text.match(/(MiniMax-M2(?:\.1)?(?:-highspeed)?|MiniMax-M3)/);
|
|
81
|
-
if (!modelMatch)
|
|
101
|
+
if (!modelMatch)
|
|
102
|
+
continue;
|
|
82
103
|
const model = modelMatch[1];
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
// 取输入/输出/缓存读取三个数;按出现顺序:Input, Output, Cache Read
|
|
105
|
+
const numbers = Array.from(text.matchAll(/\$([0-9]+(?:\.[0-9]+)?)\s*\/\s*M\s*tokens?/g)).map(m => Number(m[1]));
|
|
106
|
+
if (numbers.length < 2)
|
|
107
|
+
continue;
|
|
108
|
+
// 输入/输出 两个必备;Cache Read 可选
|
|
85
109
|
const [miss, out] = numbers;
|
|
86
110
|
const hit = numbers.length >= 3 ? numbers[2] : null;
|
|
87
|
-
if (seen.has(model))
|
|
111
|
+
if (seen.has(model))
|
|
112
|
+
continue;
|
|
88
113
|
seen.add(model);
|
|
89
|
-
rows.push({
|
|
90
|
-
model,
|
|
91
|
-
input_cache_miss_usd_per_1m: miss,
|
|
92
|
-
input_cache_hit_usd_per_1m: hit,
|
|
93
|
-
output_usd_per_1m: out
|
|
94
|
-
});
|
|
114
|
+
rows.push({ model, input_cache_miss_usd_per_1m: miss, input_cache_hit_usd_per_1m: hit, output_usd_per_1m: out });
|
|
95
115
|
}
|
|
96
116
|
return rows.length > 0 ? rows : null;
|
|
97
117
|
}
|
|
118
|
+
/** OpenAI 公开页是 React SPA(服务端只返壳 JSON 与内嵌 <script>)。
|
|
119
|
+
* 解析策略:`data/llm-pricing-snapshot-openai.html` snapshot 优先(入 git,CI 零联网);
|
|
120
|
+
* `--fetch` 模式覆盖 snapshot;解析取自 Next.js 嵌入的 `__NEXT_DATA__` JSON dump
|
|
121
|
+
* (其 `props.pageProps` 含 model price 数组),失败回退到段落文字粗解析。 */
|
|
98
122
|
export function parseOpenaiHtml(html) {
|
|
99
123
|
const fromNextData = parseOpenaiNextData(html);
|
|
100
|
-
if (fromNextData)
|
|
124
|
+
if (fromNextData)
|
|
125
|
+
return fromNextData;
|
|
101
126
|
return parseOpenaiParagraphFallback(html);
|
|
102
127
|
}
|
|
103
128
|
function parseOpenaiNextData(html) {
|
|
104
129
|
const m = html.match(/<script[^>]*id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
|
|
105
|
-
if (!m)
|
|
130
|
+
if (!m)
|
|
131
|
+
return null;
|
|
106
132
|
try {
|
|
107
133
|
const data = JSON.parse(m[1]);
|
|
108
134
|
const models = data?.props?.pageProps?.models;
|
|
109
|
-
if (!Array.isArray(models) || models.length === 0)
|
|
135
|
+
if (!Array.isArray(models) || models.length === 0)
|
|
136
|
+
return null;
|
|
110
137
|
const rows = [];
|
|
111
|
-
for (const m of models){
|
|
138
|
+
for (const m of models) {
|
|
112
139
|
const name = String(m['name'] ?? m['model'] ?? '').trim();
|
|
113
|
-
if (!name)
|
|
140
|
+
if (!name)
|
|
141
|
+
continue;
|
|
142
|
+
// 启发式:per-token 数字通常极小(<1),per-1M 数字通常 ≥ 0.1;前者要 ×1_000_000
|
|
114
143
|
const rawMiss = Number(m['input_cost_per_token'] ?? m['input_per_million'] ?? m['input']);
|
|
115
144
|
const rawOut = Number(m['output_cost_per_token'] ?? m['output_per_million'] ?? m['output']);
|
|
116
145
|
const rawHit = Number(m['cached_input_cost_per_token'] ?? m['cached_input_per_million'] ?? m['cache_read']);
|
|
117
|
-
if (!Number.isFinite(rawMiss) || !Number.isFinite(rawOut))
|
|
146
|
+
if (!Number.isFinite(rawMiss) || !Number.isFinite(rawOut))
|
|
147
|
+
continue;
|
|
118
148
|
rows.push({
|
|
119
149
|
model: name,
|
|
120
150
|
input_cache_miss_usd_per_1m: rawMiss < 1 ? rawMiss * 1_000_000 : rawMiss,
|
|
121
|
-
input_cache_hit_usd_per_1m: Number.isFinite(rawHit) ? rawHit < 1 ? rawHit * 1_000_000 : rawHit : null,
|
|
122
|
-
output_usd_per_1m: rawOut < 1 ? rawOut * 1_000_000 : rawOut
|
|
151
|
+
input_cache_hit_usd_per_1m: Number.isFinite(rawHit) ? (rawHit < 1 ? rawHit * 1_000_000 : rawHit) : null,
|
|
152
|
+
output_usd_per_1m: rawOut < 1 ? rawOut * 1_000_000 : rawOut,
|
|
123
153
|
});
|
|
124
154
|
}
|
|
125
155
|
return rows.length > 0 ? rows : null;
|
|
126
|
-
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
127
158
|
return null;
|
|
128
159
|
}
|
|
129
160
|
}
|
|
130
161
|
function parseOpenaiParagraphFallback(html) {
|
|
162
|
+
// 兜底:段落文字形如 "gpt-4o … $2.50 / 1M input … $10.00 / 1M output"
|
|
131
163
|
const rows = [];
|
|
132
164
|
const lineRe = /<p[^>]*>([\s\S]*?)<\/p>|<li[^>]*>([\s\S]*?)<\/li>/g;
|
|
133
165
|
let m;
|
|
134
|
-
while((m = lineRe.exec(html)) !== null){
|
|
166
|
+
while ((m = lineRe.exec(html)) !== null) {
|
|
135
167
|
const text = (m[1] ?? m[2] ?? '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/\s+/g, ' ').trim();
|
|
136
168
|
const modelMatch = text.match(/(gpt-4o(?:-mini)?|gpt-4\.1(?:-mini)?|gpt-5(?:-mini)?|o[1-9](?:-mini|-pro)?)/);
|
|
137
|
-
if (!modelMatch)
|
|
138
|
-
|
|
139
|
-
|
|
169
|
+
if (!modelMatch)
|
|
170
|
+
continue;
|
|
171
|
+
const numbers = Array.from(text.matchAll(/\$([0-9]+(?:\.[0-9]+)?)\s*\/\s*(?:1M|M)/g)).map(x => Number(x[1]));
|
|
172
|
+
if (numbers.length < 2)
|
|
173
|
+
continue;
|
|
140
174
|
const [miss, out] = numbers;
|
|
141
|
-
rows.push({
|
|
142
|
-
model: modelMatch[1],
|
|
143
|
-
input_cache_miss_usd_per_1m: miss,
|
|
144
|
-
input_cache_hit_usd_per_1m: null,
|
|
145
|
-
output_usd_per_1m: out
|
|
146
|
-
});
|
|
175
|
+
rows.push({ model: modelMatch[1], input_cache_miss_usd_per_1m: miss, input_cache_hit_usd_per_1m: null, output_usd_per_1m: out });
|
|
147
176
|
}
|
|
148
177
|
return rows.length > 0 ? rows : null;
|
|
149
178
|
}
|
|
179
|
+
/** Anthropic pricing 页同样 SPA,内嵌 `__NEXT_DATA__` 形态可能变;双路径同 OpenAI。 */
|
|
150
180
|
function parseAnthropicHtml(html) {
|
|
151
181
|
const fromNextData = parseAnthropicNextData(html);
|
|
152
|
-
if (fromNextData)
|
|
182
|
+
if (fromNextData)
|
|
183
|
+
return fromNextData;
|
|
153
184
|
return parseAnthropicParagraphFallback(html);
|
|
154
185
|
}
|
|
155
186
|
function parseAnthropicNextData(html) {
|
|
156
187
|
const m = html.match(/<script[^>]*id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
|
|
157
|
-
if (!m)
|
|
188
|
+
if (!m)
|
|
189
|
+
return null;
|
|
158
190
|
try {
|
|
159
191
|
const data = JSON.parse(m[1]);
|
|
160
192
|
const pricing = data?.props?.pageProps?.pricing;
|
|
161
|
-
if (!Array.isArray(pricing) || pricing.length === 0)
|
|
193
|
+
if (!Array.isArray(pricing) || pricing.length === 0)
|
|
194
|
+
return null;
|
|
162
195
|
const rows = [];
|
|
163
|
-
for (const p of pricing){
|
|
196
|
+
for (const p of pricing) {
|
|
164
197
|
const name = String(p['model'] ?? p['name'] ?? '').trim();
|
|
165
|
-
if (!name)
|
|
198
|
+
if (!name)
|
|
199
|
+
continue;
|
|
166
200
|
const miss = Number(p['input_cost_per_mtok'] ?? p['input']);
|
|
167
201
|
const out = Number(p['output_cost_per_mtok'] ?? p['output']);
|
|
168
202
|
const hit = Number(p['cache_read_cost_per_mtok'] ?? p['cache_read']);
|
|
169
|
-
if (!Number.isFinite(miss) || !Number.isFinite(out))
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
input_cache_miss_usd_per_1m: miss,
|
|
173
|
-
input_cache_hit_usd_per_1m: Number.isFinite(hit) ? hit : null,
|
|
174
|
-
output_usd_per_1m: out
|
|
175
|
-
});
|
|
203
|
+
if (!Number.isFinite(miss) || !Number.isFinite(out))
|
|
204
|
+
continue;
|
|
205
|
+
rows.push({ model: name, input_cache_miss_usd_per_1m: miss, input_cache_hit_usd_per_1m: Number.isFinite(hit) ? hit : null, output_usd_per_1m: out });
|
|
176
206
|
}
|
|
177
207
|
return rows.length > 0 ? rows : null;
|
|
178
|
-
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
179
210
|
return null;
|
|
180
211
|
}
|
|
181
212
|
}
|
|
@@ -183,103 +214,71 @@ function parseAnthropicParagraphFallback(html) {
|
|
|
183
214
|
const rows = [];
|
|
184
215
|
const lineRe = /<p[^>]*>([\s\S]*?)<\/p>|<li[^>]*>([\s\S]*?)<\/li>/g;
|
|
185
216
|
let m;
|
|
186
|
-
while((m = lineRe.exec(html)) !== null){
|
|
217
|
+
while ((m = lineRe.exec(html)) !== null) {
|
|
187
218
|
const text = (m[1] ?? m[2] ?? '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/\s+/g, ' ').trim();
|
|
188
219
|
const modelMatch = text.match(/(claude-(?:opus|sonnet|haiku)-[0-9](?:\.[0-9])?(?:-[0-9]+k)?)/);
|
|
189
|
-
if (!modelMatch)
|
|
190
|
-
|
|
191
|
-
|
|
220
|
+
if (!modelMatch)
|
|
221
|
+
continue;
|
|
222
|
+
const numbers = Array.from(text.matchAll(/\$([0-9]+(?:\.[0-9]+)?)\s*\/\s*(?:1M|M|MT)/g)).map(x => Number(x[1]));
|
|
223
|
+
if (numbers.length < 2)
|
|
224
|
+
continue;
|
|
192
225
|
const [miss, out] = numbers;
|
|
193
|
-
rows.push({
|
|
194
|
-
model: modelMatch[1],
|
|
195
|
-
input_cache_miss_usd_per_1m: miss,
|
|
196
|
-
input_cache_hit_usd_per_1m: null,
|
|
197
|
-
output_usd_per_1m: out
|
|
198
|
-
});
|
|
226
|
+
rows.push({ model: modelMatch[1], input_cache_miss_usd_per_1m: miss, input_cache_hit_usd_per_1m: null, output_usd_per_1m: out });
|
|
199
227
|
}
|
|
200
228
|
return rows.length > 0 ? rows : null;
|
|
201
229
|
}
|
|
202
230
|
function diffRows(baseline, current) {
|
|
203
|
-
const baseMap = new Map(baseline.map(
|
|
204
|
-
|
|
205
|
-
r
|
|
206
|
-
]));
|
|
207
|
-
const currMap = new Map(current.map((r)=>[
|
|
208
|
-
r.model,
|
|
209
|
-
r
|
|
210
|
-
]));
|
|
231
|
+
const baseMap = new Map(baseline.map(r => [r.model, r]));
|
|
232
|
+
const currMap = new Map(current.map(r => [r.model, r]));
|
|
211
233
|
const drift = [];
|
|
212
|
-
for (const [model, curr] of currMap){
|
|
234
|
+
for (const [model, curr] of currMap) {
|
|
213
235
|
const base = baseMap.get(model);
|
|
214
236
|
if (!base) {
|
|
215
|
-
drift.push({
|
|
216
|
-
model,
|
|
217
|
-
field: 'input_cache_miss_usd_per_1m',
|
|
218
|
-
from: null,
|
|
219
|
-
to: curr.input_cache_miss_usd_per_1m,
|
|
220
|
-
direction: 'new'
|
|
221
|
-
});
|
|
237
|
+
drift.push({ model, field: 'input_cache_miss_usd_per_1m', from: null, to: curr.input_cache_miss_usd_per_1m, direction: 'new' });
|
|
222
238
|
continue;
|
|
223
239
|
}
|
|
224
240
|
if (base.input_cache_miss_usd_per_1m !== curr.input_cache_miss_usd_per_1m) {
|
|
225
|
-
drift.push({
|
|
226
|
-
model,
|
|
227
|
-
field: 'input_cache_miss_usd_per_1m',
|
|
228
|
-
from: base.input_cache_miss_usd_per_1m,
|
|
229
|
-
to: curr.input_cache_miss_usd_per_1m,
|
|
230
|
-
direction: curr.input_cache_miss_usd_per_1m > base.input_cache_miss_usd_per_1m ? 'up' : 'down'
|
|
231
|
-
});
|
|
241
|
+
drift.push({ model, field: 'input_cache_miss_usd_per_1m', from: base.input_cache_miss_usd_per_1m, to: curr.input_cache_miss_usd_per_1m, direction: curr.input_cache_miss_usd_per_1m > base.input_cache_miss_usd_per_1m ? 'up' : 'down' });
|
|
232
242
|
}
|
|
233
243
|
if (base.input_cache_hit_usd_per_1m !== curr.input_cache_hit_usd_per_1m) {
|
|
234
|
-
drift.push({
|
|
235
|
-
model,
|
|
236
|
-
field: 'input_cache_hit_usd_per_1m',
|
|
237
|
-
from: base.input_cache_hit_usd_per_1m,
|
|
238
|
-
to: curr.input_cache_hit_usd_per_1m,
|
|
239
|
-
direction: (curr.input_cache_hit_usd_per_1m ?? 0) > (base.input_cache_hit_usd_per_1m ?? 0) ? 'up' : 'down'
|
|
240
|
-
});
|
|
244
|
+
drift.push({ model, field: 'input_cache_hit_usd_per_1m', from: base.input_cache_hit_usd_per_1m, to: curr.input_cache_hit_usd_per_1m, direction: (curr.input_cache_hit_usd_per_1m ?? 0) > (base.input_cache_hit_usd_per_1m ?? 0) ? 'up' : 'down' });
|
|
241
245
|
}
|
|
242
246
|
if (base.output_usd_per_1m !== curr.output_usd_per_1m) {
|
|
243
|
-
drift.push({
|
|
244
|
-
model,
|
|
245
|
-
field: 'output_usd_per_1m',
|
|
246
|
-
from: base.output_usd_per_1m,
|
|
247
|
-
to: curr.output_usd_per_1m,
|
|
248
|
-
direction: curr.output_usd_per_1m > base.output_usd_per_1m ? 'up' : 'down'
|
|
249
|
-
});
|
|
247
|
+
drift.push({ model, field: 'output_usd_per_1m', from: base.output_usd_per_1m, to: curr.output_usd_per_1m, direction: curr.output_usd_per_1m > base.output_usd_per_1m ? 'up' : 'down' });
|
|
250
248
|
}
|
|
251
249
|
}
|
|
252
|
-
for (const [model, base] of baseMap){
|
|
250
|
+
for (const [model, base] of baseMap) {
|
|
253
251
|
if (!currMap.has(model)) {
|
|
254
|
-
drift.push({
|
|
255
|
-
model,
|
|
256
|
-
field: 'input_cache_miss_usd_per_1m',
|
|
257
|
-
from: base.input_cache_miss_usd_per_1m,
|
|
258
|
-
to: null,
|
|
259
|
-
direction: 'removed'
|
|
260
|
-
});
|
|
252
|
+
drift.push({ model, field: 'input_cache_miss_usd_per_1m', from: base.input_cache_miss_usd_per_1m, to: null, direction: 'removed' });
|
|
261
253
|
}
|
|
262
254
|
}
|
|
263
255
|
return drift;
|
|
264
256
|
}
|
|
265
257
|
function loadBaseline(path) {
|
|
266
|
-
if (!existsSync(path))
|
|
258
|
+
if (!existsSync(path))
|
|
259
|
+
return null;
|
|
267
260
|
try {
|
|
268
261
|
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
269
|
-
if (raw['schema_version'] !== 'gotry_llm_price_baseline_v1')
|
|
262
|
+
if (raw['schema_version'] !== 'gotry_llm_price_baseline_v1')
|
|
263
|
+
throw new Error(`baseline ${path} schema_version must be gotry_llm_price_baseline_v1, got ${String(raw['schema_version'])}`);
|
|
270
264
|
const rows = raw['rows'];
|
|
271
|
-
if (!Array.isArray(rows))
|
|
265
|
+
if (!Array.isArray(rows))
|
|
266
|
+
throw new Error(`baseline ${path} rows must be array`);
|
|
272
267
|
return raw;
|
|
273
|
-
}
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
274
270
|
return null;
|
|
275
271
|
}
|
|
276
272
|
}
|
|
273
|
+
/** 读 snapshot HTML 文件;不存在或空 → null。snapshot 由 `--fetch` 模式首次写盘(founder 手动 PR)。 */
|
|
277
274
|
function loadSnapshot(path) {
|
|
278
|
-
if (!existsSync(path))
|
|
275
|
+
if (!existsSync(path))
|
|
276
|
+
return null;
|
|
279
277
|
try {
|
|
280
278
|
const text = readFileSync(path, 'utf8');
|
|
281
279
|
return text.length > 0 ? text : null;
|
|
282
|
-
}
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
283
282
|
return null;
|
|
284
283
|
}
|
|
285
284
|
}
|
|
@@ -291,7 +290,7 @@ function writeBaseline(path, rows, sourceUrl) {
|
|
|
291
290
|
schema_version: 'gotry_llm_price_baseline_v1',
|
|
292
291
|
as_of: new Date().toISOString(),
|
|
293
292
|
source_url: sourceUrl,
|
|
294
|
-
rows
|
|
293
|
+
rows,
|
|
295
294
|
};
|
|
296
295
|
writeFileSync(path, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
297
296
|
}
|
|
@@ -299,107 +298,70 @@ export async function runPriceDriftWatch(options) {
|
|
|
299
298
|
const observedAt = options.observedAt().toISOString();
|
|
300
299
|
const providers = {};
|
|
301
300
|
const providerSpecs = options.providers ?? DEFAULT_PROVIDERS;
|
|
302
|
-
const fetcher = options.fetcher ?? ((url)=>fetchWithTimeout(url, FETCH_TIMEOUT_MS));
|
|
303
|
-
for (const spec of providerSpecs){
|
|
301
|
+
const fetcher = options.fetcher ?? ((url) => fetchWithTimeout(url, FETCH_TIMEOUT_MS));
|
|
302
|
+
for (const spec of providerSpecs) {
|
|
304
303
|
const baseline = loadBaseline(spec.baseline_path);
|
|
305
304
|
const snapshot = spec.snapshot_path ? loadSnapshot(spec.snapshot_path) : null;
|
|
306
305
|
if (options.mode === 'offline') {
|
|
306
|
+
// 离线模式两种来源:
|
|
307
|
+
// (a) 有 snapshot → 走 snapshot.html parse 出 current,与 baseline diff(零联网,CI 心形)
|
|
308
|
+
// (b) 无 snapshot → 只能报 baseline 状态(无东西可 diff,标 unchanged + 提示)
|
|
307
309
|
if (!baseline) {
|
|
308
|
-
providers[spec.id] = {
|
|
309
|
-
status: 'skipped_no_baseline',
|
|
310
|
-
source_url: spec.source_url,
|
|
311
|
-
reason: `no baseline fixture at ${spec.baseline_path}; run --fetch once to seed`
|
|
312
|
-
};
|
|
310
|
+
providers[spec.id] = { status: 'skipped_no_baseline', source_url: spec.source_url, reason: `no baseline fixture at ${spec.baseline_path}; run --fetch once to seed` };
|
|
313
311
|
continue;
|
|
314
312
|
}
|
|
315
313
|
if (snapshot === null) {
|
|
316
314
|
if (spec.snapshot_path) {
|
|
317
|
-
providers[spec.id] = {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
};
|
|
322
|
-
} else {
|
|
323
|
-
providers[spec.id] = {
|
|
324
|
-
status: 'unchanged',
|
|
325
|
-
source_url: spec.source_url,
|
|
326
|
-
reason: 'offline mode; baseline present, run --fetch to compare against current official prices'
|
|
327
|
-
};
|
|
315
|
+
providers[spec.id] = { status: 'skipped_no_snapshot', source_url: spec.source_url, reason: `no snapshot at ${spec.snapshot_path}; run --fetch once to seed (or: founder manually save official pricing page HTML as the snapshot)` };
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
providers[spec.id] = { status: 'unchanged', source_url: spec.source_url, reason: 'offline mode; baseline present, run --fetch to compare against current official prices' };
|
|
328
319
|
}
|
|
329
320
|
continue;
|
|
330
321
|
}
|
|
331
322
|
const currentFromSnapshot = spec.parse(snapshot);
|
|
332
323
|
if (!currentFromSnapshot) {
|
|
333
|
-
providers[spec.id] = {
|
|
334
|
-
status: 'skipped_parse_error',
|
|
335
|
-
source_url: spec.source_url,
|
|
336
|
-
reason: 'snapshot present but parser failed to extract rows; refresh snapshot or fix parser'
|
|
337
|
-
};
|
|
324
|
+
providers[spec.id] = { status: 'skipped_parse_error', source_url: spec.source_url, reason: 'snapshot present but parser failed to extract rows; refresh snapshot or fix parser' };
|
|
338
325
|
continue;
|
|
339
326
|
}
|
|
340
327
|
const driftFromSnapshot = diffRows(baseline.rows, currentFromSnapshot) ?? [];
|
|
341
328
|
if (driftFromSnapshot.length === 0) {
|
|
342
|
-
providers[spec.id] = {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
};
|
|
347
|
-
} else {
|
|
348
|
-
providers[spec.id] = {
|
|
349
|
-
status: 'drift',
|
|
350
|
-
source_url: spec.source_url,
|
|
351
|
-
fetched_at: observedAt,
|
|
352
|
-
drift: driftFromSnapshot,
|
|
353
|
-
reason: 'offline diff baseline vs snapshot'
|
|
354
|
-
};
|
|
329
|
+
providers[spec.id] = { status: 'unchanged', source_url: spec.source_url, reason: 'offline; baseline == snapshot-derived current' };
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
providers[spec.id] = { status: 'drift', source_url: spec.source_url, fetched_at: observedAt, drift: driftFromSnapshot, reason: 'offline diff baseline vs snapshot' };
|
|
355
333
|
}
|
|
356
334
|
continue;
|
|
357
335
|
}
|
|
336
|
+
// fetch mode
|
|
358
337
|
let html;
|
|
359
338
|
try {
|
|
360
339
|
html = await fetcher(spec.source_url);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
source_url: spec.source_url,
|
|
365
|
-
reason: `fetch failed: ${error.message}`
|
|
366
|
-
};
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
providers[spec.id] = { status: 'skipped_no_network', source_url: spec.source_url, reason: `fetch failed: ${error.message}` };
|
|
367
343
|
continue;
|
|
368
344
|
}
|
|
369
|
-
|
|
345
|
+
// snapshot 写盘:OpenAI/Anthropic SPA 的真实状态在 snapshot 文件里(founder 手动 PR)
|
|
346
|
+
if (spec.snapshot_path)
|
|
347
|
+
writeSnapshot(spec.snapshot_path, html);
|
|
370
348
|
const current = spec.parse(html);
|
|
371
349
|
if (!current) {
|
|
372
|
-
providers[spec.id] = {
|
|
373
|
-
status: 'skipped_parse_error',
|
|
374
|
-
source_url: spec.source_url,
|
|
375
|
-
reason: 'parser did not extract any rows; snapshot was written anyway (manual review of fixture + parser required)'
|
|
376
|
-
};
|
|
350
|
+
providers[spec.id] = { status: 'skipped_parse_error', source_url: spec.source_url, reason: 'parser did not extract any rows; snapshot was written anyway (manual review of fixture + parser required)' };
|
|
377
351
|
continue;
|
|
378
352
|
}
|
|
379
353
|
if (!baseline) {
|
|
354
|
+
// 首次 fetch 写 fixture
|
|
380
355
|
writeBaseline(spec.baseline_path, current, spec.source_url);
|
|
381
|
-
providers[spec.id] = {
|
|
382
|
-
status: 'fetched_no_table_entry',
|
|
383
|
-
source_url: spec.source_url,
|
|
384
|
-
fetched_at: observedAt,
|
|
385
|
-
reason: 'first fetch; baseline fixture written; no current llm-price-table.json entry (add a PR if you want this provider in nightly-evidence cost math)'
|
|
386
|
-
};
|
|
356
|
+
providers[spec.id] = { status: 'fetched_no_table_entry', source_url: spec.source_url, fetched_at: observedAt, reason: 'first fetch; baseline fixture written; no current llm-price-table.json entry (add a PR if you want this provider in nightly-evidence cost math)' };
|
|
387
357
|
continue;
|
|
388
358
|
}
|
|
389
359
|
const drift = diffRows(baseline.rows, current) ?? [];
|
|
390
360
|
if (drift.length === 0) {
|
|
391
|
-
providers[spec.id] = {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
};
|
|
396
|
-
} else {
|
|
397
|
-
providers[spec.id] = {
|
|
398
|
-
status: 'drift',
|
|
399
|
-
source_url: spec.source_url,
|
|
400
|
-
fetched_at: observedAt,
|
|
401
|
-
drift
|
|
402
|
-
};
|
|
361
|
+
providers[spec.id] = { status: 'unchanged', source_url: spec.source_url, fetched_at: observedAt };
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
providers[spec.id] = { status: 'drift', source_url: spec.source_url, fetched_at: observedAt, drift };
|
|
403
365
|
}
|
|
404
366
|
}
|
|
405
367
|
const suggestedPrParagraph = renderSuggestedPr(providers, observedAt);
|
|
@@ -408,26 +370,27 @@ export async function runPriceDriftWatch(options) {
|
|
|
408
370
|
observed_at: observedAt,
|
|
409
371
|
mode: options.mode,
|
|
410
372
|
providers,
|
|
411
|
-
suggested_pr_paragraph: suggestedPrParagraph
|
|
373
|
+
suggested_pr_paragraph: suggestedPrParagraph,
|
|
412
374
|
};
|
|
413
375
|
}
|
|
414
376
|
function renderSuggestedPr(providers, observedAt) {
|
|
415
|
-
const lines = [
|
|
416
|
-
|
|
417
|
-
];
|
|
418
|
-
for (const [id, report] of Object.entries(providers)){
|
|
377
|
+
const lines = [`## LLM 价格漂移监测(${observedAt})`];
|
|
378
|
+
for (const [id, report] of Object.entries(providers)) {
|
|
419
379
|
if (report.status === 'drift' && report.drift && report.drift.length > 0) {
|
|
420
380
|
const driftItems = report.drift;
|
|
421
381
|
lines.push(`- **${id}**(${report.source_url}):${driftItems.length} 项变动`);
|
|
422
|
-
for (const d of driftItems){
|
|
382
|
+
for (const d of driftItems) {
|
|
423
383
|
lines.push(` - \`${d.model}\` ${d.field}: ${d.from ?? '∅'} → ${d.to ?? '∅'} (${d.direction})`);
|
|
424
384
|
}
|
|
425
385
|
lines.push(' - 建议:按 ADR-11 复核,人工提 PR 改 `data/llm-price-table.json`。');
|
|
426
|
-
}
|
|
386
|
+
}
|
|
387
|
+
else if (report.status === 'unchanged') {
|
|
427
388
|
lines.push(`- **${id}**:无变动`);
|
|
428
|
-
}
|
|
389
|
+
}
|
|
390
|
+
else if (report.status === 'fetched_no_table_entry') {
|
|
429
391
|
lines.push(`- **${id}**:首次 fetch,baseline 已写入(${report.reason ?? ''})`);
|
|
430
|
-
}
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
431
394
|
lines.push(`- **${id}** [${report.status}]:${report.reason ?? ''}`);
|
|
432
395
|
}
|
|
433
396
|
}
|
|
@@ -436,18 +399,17 @@ function renderSuggestedPr(providers, observedAt) {
|
|
|
436
399
|
return lines.join('\n');
|
|
437
400
|
}
|
|
438
401
|
function formatMarkdown(report) {
|
|
439
|
-
const lines = [
|
|
440
|
-
|
|
441
|
-
''
|
|
442
|
-
];
|
|
443
|
-
for (const [id, r] of Object.entries(report.providers)){
|
|
402
|
+
const lines = [`# LLM 价格漂移监测(${report.observed_at},mode=${report.mode})`, ''];
|
|
403
|
+
for (const [id, r] of Object.entries(report.providers)) {
|
|
444
404
|
lines.push(`## ${id} [${r.status}]`);
|
|
445
405
|
lines.push(`source: ${r.source_url}`);
|
|
446
|
-
if (r.reason)
|
|
406
|
+
if (r.reason)
|
|
407
|
+
lines.push(`reason: ${r.reason}`);
|
|
447
408
|
if (r.drift && r.drift.length > 0) {
|
|
448
409
|
lines.push('| model | field | from | to | direction |');
|
|
449
410
|
lines.push('|---|---|---|---|---|');
|
|
450
|
-
for (const d of r.drift)
|
|
411
|
+
for (const d of r.drift)
|
|
412
|
+
lines.push(`| \`${d.model}\` | ${d.field} | ${d.from ?? '∅'} | ${d.to ?? '∅'} | ${d.direction} |`);
|
|
451
413
|
}
|
|
452
414
|
lines.push('');
|
|
453
415
|
}
|
|
@@ -463,29 +425,23 @@ function arg(name) {
|
|
|
463
425
|
async function main() {
|
|
464
426
|
const asJson = (arg('--format') ?? 'markdown') === 'json';
|
|
465
427
|
const mode = process.argv.includes('--fetch') ? 'fetch' : 'offline';
|
|
466
|
-
mkdirSync('gotry-state', {
|
|
467
|
-
recursive: true
|
|
468
|
-
});
|
|
428
|
+
mkdirSync('gotry-state', { recursive: true });
|
|
469
429
|
try {
|
|
470
|
-
const report = await runPriceDriftWatch({
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const payload = {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
else console.error(`price-drift-watch error: ${error.message}`);
|
|
430
|
+
const report = await runPriceDriftWatch({ mode, observedAt: () => new Date() });
|
|
431
|
+
if (asJson)
|
|
432
|
+
console.log(JSON.stringify(report, null, 2));
|
|
433
|
+
else
|
|
434
|
+
console.log(formatMarkdown(report));
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
const payload = { state: 'error', error: error.message };
|
|
438
|
+
if (asJson)
|
|
439
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
440
|
+
else
|
|
441
|
+
console.error(`price-drift-watch error: ${error.message}`);
|
|
483
442
|
process.exitCode = 1;
|
|
484
443
|
}
|
|
485
444
|
}
|
|
486
445
|
if (process.argv[1]?.endsWith('price-drift-watch.ts')) {
|
|
487
446
|
await main();
|
|
488
447
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
//# sourceURL=ts/scripts/price-drift-watch.ts
|