@danceiny/gotry 0.0.1-rc.14 → 0.0.1-rc.16

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.
Files changed (142) hide show
  1. package/README.md +26 -20
  2. package/README.zh-CN.md +27 -21
  3. package/bin/gotry-bootstrap.js +331 -3
  4. package/cordis.gotry-patch.yml +10 -0
  5. package/data/airline-airports.json +39 -0
  6. package/dist/capabilities/agent-reach-deep.js +1 -1
  7. package/dist/capabilities/agent-reach.js +1 -1
  8. package/dist/capabilities/anything.js +1 -1
  9. package/dist/capabilities/artifacts.js +217 -0
  10. package/dist/capabilities/effect.js +293 -0
  11. package/dist/capabilities/fact-log.js +41 -0
  12. package/dist/capabilities/flyai.js +1 -1
  13. package/dist/capabilities/hbcli.js +43 -1
  14. package/dist/capabilities/incident-log.js +1 -1
  15. package/dist/capabilities/opensky.js +1 -1
  16. package/dist/capabilities/resilience.js +90 -0
  17. package/dist/capabilities/session/action-cache.js +1 -1
  18. package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
  19. package/dist/capabilities/session/adapters/meituan-local.js +1 -1
  20. package/dist/capabilities/session/benchmark.js +2 -1
  21. package/dist/capabilities/session/extension-bridge.js +345 -0
  22. package/dist/capabilities/session/extension-channel.js +93 -0
  23. package/dist/capabilities/session/extract.js +1 -1
  24. package/dist/capabilities/session/health-watch.js +216 -0
  25. package/dist/capabilities/session/read-guard.js +1 -1
  26. package/dist/capabilities/session/transport.js +1 -1
  27. package/dist/capabilities/session/wizard.js +377 -0
  28. package/dist/capabilities/session-consent.js +1 -1
  29. package/dist/capabilities/session-login.js +77 -1
  30. package/dist/capabilities/session-search.js +69 -2
  31. package/dist/capabilities/weather.js +1 -1
  32. package/dist/scripts/action-cache-tests.js +1 -1
  33. package/dist/scripts/agent-reach-deep-tests.js +1 -1
  34. package/dist/scripts/agent-reach-tests.js +1 -1
  35. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  36. package/dist/scripts/anything-tests.js +1 -1
  37. package/dist/scripts/async-collect.js +1 -1
  38. package/dist/scripts/booking-saga-tests.js +187 -0
  39. package/dist/scripts/bootstrap-tests.js +49 -3
  40. package/dist/scripts/build-changelog.js +279 -0
  41. package/dist/scripts/changelog-tests.js +165 -0
  42. package/dist/scripts/companion-tests.js +1 -1
  43. package/dist/scripts/diff-test.js +1 -1
  44. package/dist/scripts/effect-tests.js +474 -0
  45. package/dist/scripts/engine-run.js +1 -1
  46. package/dist/scripts/engine-tests.js +1 -1
  47. package/dist/scripts/extension-tests.js +468 -0
  48. package/dist/scripts/fact-gate-tests.js +318 -0
  49. package/dist/scripts/flyai-tests.js +1 -1
  50. package/dist/scripts/hbcli-e2e-tests.js +180 -0
  51. package/dist/scripts/hbcli-tests.js +1 -1
  52. package/dist/scripts/health-watch-cli.js +51 -0
  53. package/dist/scripts/i18n-tests.js +1 -1
  54. package/dist/scripts/incident-tests.js +1 -1
  55. package/dist/scripts/journey-tests.js +1 -1
  56. package/dist/scripts/ledger-tests.js +1 -1
  57. package/dist/scripts/ledger-workflow-crash.js +1 -1
  58. package/dist/scripts/memory-capture-tests.js +1 -1
  59. package/dist/scripts/memory-decay-tests.js +1 -1
  60. package/dist/scripts/memory-metrics.js +1 -1
  61. package/dist/scripts/memory-value-report.js +1 -1
  62. package/dist/scripts/nightly-evidence-tests.js +186 -0
  63. package/dist/scripts/nightly-evidence.js +289 -0
  64. package/dist/scripts/nudge-digest.js +1 -1
  65. package/dist/scripts/onboarding-tests.js +218 -0
  66. package/dist/scripts/opensky-check.js +1 -1
  67. package/dist/scripts/opensky-tests.js +1 -1
  68. package/dist/scripts/price-drift-tests.js +441 -0
  69. package/dist/scripts/price-drift-watch.js +491 -0
  70. package/dist/scripts/probe-poi-tests.js +1 -1
  71. package/dist/scripts/product-metrics.js +1 -1
  72. package/dist/scripts/publish-preverify.js +1 -1
  73. package/dist/scripts/realtime-pricing-tests.js +1 -1
  74. package/dist/scripts/replay-async.js +1 -1
  75. package/dist/scripts/replay-real.js +1 -1
  76. package/dist/scripts/replay.js +1 -1
  77. package/dist/scripts/session-attach-diagnose.js +1 -1
  78. package/dist/scripts/session-attach-poc.js +1 -1
  79. package/dist/scripts/session-benchmark.js +1 -1
  80. package/dist/scripts/session-extract-tests.js +1 -1
  81. package/dist/scripts/session-login.js +1 -1
  82. package/dist/scripts/session-tests.js +1 -1
  83. package/dist/scripts/skeleton-check.js +1 -1
  84. package/dist/scripts/skeleton-integration-test.js +1 -1
  85. package/dist/scripts/skills-contract-tests.js +1 -1
  86. package/dist/scripts/smoke.js +159 -1
  87. package/dist/scripts/state-cli-tests.js +1 -1
  88. package/dist/scripts/state-cli.js +1 -1
  89. package/dist/scripts/time-eval-tests.js +1 -1
  90. package/dist/scripts/travel-timeline-tests.js +1 -1
  91. package/dist/scripts/unified-tests.js +1 -1
  92. package/dist/scripts/weather-tests.js +1 -1
  93. package/dist/scripts/wizard-bootstrap.js +32 -0
  94. package/dist/scripts/z3-race-tests.js +1 -1
  95. package/dist/src/artifact-gate.js +310 -0
  96. package/dist/src/bookable-facts.js +349 -0
  97. package/dist/src/booking-saga.js +153 -0
  98. package/dist/src/bridge.js +1 -1
  99. package/dist/src/companions.js +1 -1
  100. package/dist/src/contracts.js +1 -1
  101. package/dist/src/dsh-llm.js +31 -7
  102. package/dist/src/engine.js +1 -1
  103. package/dist/src/i18n.js +1 -1
  104. package/dist/src/index.js +322 -46
  105. package/dist/src/journey.js +1 -1
  106. package/dist/src/loop.js +1 -1
  107. package/dist/src/memory-capture.js +1 -1
  108. package/dist/src/memory-decay.js +1 -1
  109. package/dist/src/memory-utility.js +1 -1
  110. package/dist/src/mock-llm.js +1 -1
  111. package/dist/src/model.js +1 -1
  112. package/dist/src/realtime-pricing.js +14 -7
  113. package/dist/src/slot-spec.js +1 -1
  114. package/dist/src/state-ledger.js +1 -1
  115. package/dist/src/time-anchor.js +1 -1
  116. package/dist/src/tool-packet.js +1 -1
  117. package/dist/src/travel-slots.js +1 -1
  118. package/dist/src/travel-timeline.js +1 -1
  119. package/dist/src/unified.js +1 -1
  120. package/dist/src/wish-pool.js +1 -1
  121. package/dist/src/z3-shared.js +1 -1
  122. package/extension/README.md +26 -0
  123. package/extension/background.js +160 -0
  124. package/extension/content-bridge.js +41 -0
  125. package/extension/content-main.js +62 -0
  126. package/extension/manifest.json +32 -0
  127. package/package.json +8 -2
  128. package/ts/capabilities/artifacts.ts +235 -0
  129. package/ts/capabilities/fact-log.ts +54 -0
  130. package/ts/capabilities/hbcli.ts +46 -0
  131. package/ts/capabilities/session/benchmark.ts +4 -1
  132. package/ts/capabilities/session/extension-bridge.ts +354 -0
  133. package/ts/capabilities/session/extension-channel.ts +92 -0
  134. package/ts/capabilities/session/health-watch.ts +257 -0
  135. package/ts/capabilities/session/wizard.ts +282 -0
  136. package/ts/capabilities/session-login.ts +53 -2
  137. package/ts/capabilities/session-search.ts +68 -3
  138. package/ts/src/artifact-gate.ts +365 -0
  139. package/ts/src/bookable-facts.ts +557 -0
  140. package/ts/src/booking-saga.ts +133 -0
  141. package/ts/src/dsh-llm.ts +40 -3
  142. package/ts/src/index.ts +230 -28
@@ -0,0 +1,186 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { buildNightlyRunRecord, loadPriceTable, loadPromptSet, priceRunCost, runNightlyEvidence } from './nightly-evidence.js';
6
+ import { parseNightlyRun } from './product-metrics.js';
7
+ let passed = 0;
8
+ function ok(label) {
9
+ passed += 1;
10
+ console.log(` ok ${label}`);
11
+ }
12
+ const priceTable = loadPriceTable('data/llm-price-table.json');
13
+ const promptSet = loadPromptSet('data/m3-nightly-prompts.json');
14
+ assert.equal(priceTable.schema_version, 'gotry_llm_price_table_v2');
15
+ assert.equal(promptSet.schema_version, 'gotry_m3_nightly_prompt_set_v1');
16
+ assert.ok(promptSet.turns.length >= 1);
17
+ assert.ok(priceTable.prices['deepseek-v4-flash'], '裸键 deepseek-v4-flash 必须可查');
18
+ assert.ok(priceTable.prices['MiniMax-M3'], '裸键 MiniMax-M3 必须可查(provider 为单一时透明)');
19
+ assert.ok(priceTable.prices['minimax:MiniMax-M3'], 'qualified 键 minimax:MiniMax-M3 必须可查');
20
+ assert.ok(priceTable.aliases['deepseek-chat'], 'deepseek-chat 必须在 aliases 表');
21
+ assert.equal(priceTable.aliases['deepseek-chat'], 'deepseek-v4-flash');
22
+ ok('v2 schema 载入 + 裸键 + qualified 键 + alias 双查找');
23
+ for (const [name, entry] of Object.entries(priceTable.prices)){
24
+ assert.ok(entry.input_cache_hit_usd_per_1m_peak <= entry.input_cache_miss_usd_per_1m_peak, `${name} hit<=miss`);
25
+ assert.ok(entry.output_usd_per_1m_offpeak <= entry.output_usd_per_1m_peak, `${name} offpeak<=peak`);
26
+ }
27
+ ok('价目保守上界不变量(hit<=miss, offpeak<=peak)');
28
+ const baseUsage = {
29
+ calls: 3,
30
+ inputTokens: 1_000_000,
31
+ outputTokens: 1_000_000,
32
+ inputCacheHitTokens: 0,
33
+ inputCacheMissTokens: 1_000_000,
34
+ responsesMissingUsage: 0
35
+ };
36
+ const cost = priceRunCost('deepseek-chat', baseUsage, priceTable);
37
+ assert.equal(cost, 1.76);
38
+ ok('peak miss 换算 0.44+1.32=1.76(经 alias)');
39
+ const hitUsage = {
40
+ ...baseUsage,
41
+ inputCacheHitTokens: 1_000_000,
42
+ inputCacheMissTokens: 0
43
+ };
44
+ assert.equal(priceRunCost('deepseek-chat', hitUsage, priceTable), 1.334);
45
+ ok('peak hit 换算 0.014+1.32=1.334');
46
+ const m3cost = priceRunCost('MiniMax-M3', baseUsage, priceTable);
47
+ assert.equal(m3cost, 3.00);
48
+ ok('MiniMax-M3 peak miss 换算 0.60+2.40=3.00');
49
+ const m3hit = priceRunCost('MiniMax-M3', hitUsage, priceTable);
50
+ assert.equal(m3hit, 2.52);
51
+ ok('MiniMax-M3 peak hit 换算 0.12+2.40=2.52');
52
+ const m3prov = priceTable.prices['minimax:MiniMax-M3'];
53
+ assert.equal(m3prov.input_cache_miss_usd_per_1m_peak, m3prov.input_cache_miss_usd_per_1m_offpeak, 'MiniMax-M3 flat 纪律:peak==offpeak miss');
54
+ assert.equal(m3prov.input_cache_hit_usd_per_1m_peak, m3prov.input_cache_hit_usd_per_1m_offpeak, 'MiniMax-M3 flat 纪律:peak==offpeak hit');
55
+ assert.equal(m3prov.output_usd_per_1m_peak, m3prov.output_usd_per_1m_offpeak, 'MiniMax-M3 flat 纪律:peak==offpeak output');
56
+ ok('MiniMax flat_no_offpeak 纪律同价守门');
57
+ assert.throws(()=>priceRunCost('unknown-model-xyz', baseUsage, priceTable), /no entry for model/);
58
+ ok('未知模型名 → fail-closed');
59
+ const dangling = {
60
+ ...priceTable,
61
+ aliases: {
62
+ 'deepseek-chat': 'not-a-model'
63
+ }
64
+ };
65
+ assert.throws(()=>priceRunCost('deepseek-chat', baseUsage, dangling), /has no entry/);
66
+ assert.throws(()=>priceRunCost('deepseek-chat', {
67
+ ...baseUsage,
68
+ responsesMissingUsage: 1
69
+ }, priceTable), /cost unprovable/);
70
+ const tmpSchemaDir = mkdtempSync(join(tmpdir(), 'nightly-bad-schema-'));
71
+ const badSchemaPath = join(tmpSchemaDir, 'bad.json');
72
+ try {
73
+ writeFileSync(badSchemaPath, JSON.stringify({
74
+ schema_version: 'gotry_llm_price_table_v99',
75
+ prices: {}
76
+ }));
77
+ assert.throws(()=>loadPriceTable(badSchemaPath), /schema_version must be/);
78
+ ok('非法 schema_version 拒绝');
79
+ } finally{
80
+ rmSync(tmpSchemaDir, {
81
+ recursive: true,
82
+ force: true
83
+ });
84
+ }
85
+ const tmpStrategyDir = mkdtempSync(join(tmpdir(), 'nightly-bad-strategy-'));
86
+ const badStrategyPath = join(tmpStrategyDir, 'bad.json');
87
+ try {
88
+ writeFileSync(badStrategyPath, JSON.stringify({
89
+ schema_version: 'gotry_llm_price_table_v2',
90
+ providers: {
91
+ bad: {
92
+ family: 'x',
93
+ price_strategy: 'mystery_strategy',
94
+ aliases: {},
95
+ models: {
96
+ 'm': {
97
+ input_cache_miss_usd_per_1m_peak: 0.1,
98
+ input_cache_hit_usd_per_1m_peak: 0.01,
99
+ output_usd_per_1m_peak: 0.5,
100
+ input_cache_miss_usd_per_1m_offpeak: 0.1,
101
+ input_cache_hit_usd_per_1m_offpeak: 0.01,
102
+ output_usd_per_1m_offpeak: 0.5
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }));
108
+ assert.throws(()=>loadPriceTable(badStrategyPath), /price_strategy must be/);
109
+ ok('v2 非法 price_strategy 拒绝');
110
+ } finally{
111
+ rmSync(tmpStrategyDir, {
112
+ recursive: true,
113
+ force: true
114
+ });
115
+ }
116
+ ok('未知模型/悬空alias/usage缺失 fail-closed');
117
+ const executeAt = '2026-08-29T12:00:00.000Z';
118
+ const recordInput = {
119
+ schema_version: 'gotry_m3_nightly_run_v1',
120
+ executed_at: executeAt,
121
+ real_llm: true,
122
+ prompt_set_sha256: 'a'.repeat(64),
123
+ output_sha256: 'b'.repeat(64),
124
+ cost_usd: 0.25
125
+ };
126
+ const rec1 = buildNightlyRunRecord(recordInput);
127
+ const rec2 = buildNightlyRunRecord({
128
+ ...recordInput
129
+ });
130
+ assert.equal(rec1.run_key, rec2.run_key);
131
+ assert.match(rec1.run_key, /^hmac-sha256:[0-9a-f]{64}$/);
132
+ parseNightlyRun(JSON.parse(JSON.stringify(rec1)), 0);
133
+ const rec3 = buildNightlyRunRecord({
134
+ ...recordInput,
135
+ cost_usd: 0.26
136
+ });
137
+ assert.notEqual(rec3.run_key, rec1.run_key);
138
+ ok('run_key 确定性 + 已过消费方 parseNightlyRun');
139
+ const prevKey = process.env['LLM_API_KEY'];
140
+ const prevDeepseekKey = process.env['DEEPSEEK_API_KEY'];
141
+ const waitParent = mkdtempSync(join(tmpdir(), 'nightly-wait-'));
142
+ const waitRoot = join(waitParent, 'evidence', 'm3');
143
+ try {
144
+ delete process.env['LLM_API_KEY'];
145
+ delete process.env['DEEPSEEK_API_KEY'];
146
+ const waiting = await runNightlyEvidence({
147
+ evidenceRoot: waitRoot,
148
+ dryRun: false,
149
+ clock: ()=>new Date(executeAt)
150
+ });
151
+ assert.equal(waiting.state, 'waiting_external_evidence');
152
+ assert.equal(waiting.record, null);
153
+ assert.ok(waiting.reason?.includes('backoff'));
154
+ assert.equal(existsSync(waitRoot), false, 'waiting 不得创建任何证据目录');
155
+ } finally{
156
+ if (prevKey !== undefined) process.env['LLM_API_KEY'] = prevKey;
157
+ if (prevDeepseekKey !== undefined) process.env['DEEPSEEK_API_KEY'] = prevDeepseekKey;
158
+ rmSync(waitParent, {
159
+ recursive: true,
160
+ force: true
161
+ });
162
+ }
163
+ ok('无凭证 → waiting_external_evidence 且零写入');
164
+ const dryParent = mkdtempSync(join(tmpdir(), 'nightly-dry-'));
165
+ const dryRoot = join(dryParent, 'evidence', 'm3');
166
+ try {
167
+ const dry = await runNightlyEvidence({
168
+ evidenceRoot: dryRoot,
169
+ dryRun: true,
170
+ clock: ()=>new Date(executeAt)
171
+ });
172
+ assert.equal(dry.state, 'dry_run');
173
+ assert.equal(dry.record?.real_llm, false);
174
+ parseNightlyRun(JSON.parse(JSON.stringify(dry.record)), 0);
175
+ assert.equal(existsSync(dryRoot), false, 'dry-run 不得写任何证据文件');
176
+ } finally{
177
+ rmSync(dryParent, {
178
+ recursive: true,
179
+ force: true
180
+ });
181
+ }
182
+ ok('dry-run mock 全链演练 + real_llm=false + 零落盘');
183
+ console.log(`nightly-evidence tests: ${passed} 组断言全绿(offline,真跑不在 CI)`);
184
+
185
+
186
+ //# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/nightly-evidence-tests.ts
@@ -0,0 +1,289 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { createMockLlm } from '../src/mock-llm.js';
5
+ import { createOpenAICompatLlm } from '../src/dsh-llm.js';
6
+ import { newState, runTurn } from '../src/loop.js';
7
+ import { solveUnified } from '../src/unified.js';
8
+ import { realtimeSolvePort } from '../src/realtime-pricing.js';
9
+ import { parseNightlyRun } from './product-metrics.js';
10
+ const SHA256 = /^[0-9a-f]{64}$/;
11
+ const DEFAULT_BUDGET_USD = 1;
12
+ function canonical(value) {
13
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
14
+ if (typeof value === 'object' && value !== null) {
15
+ const record = value;
16
+ return `{${Object.keys(record).sort().map((key)=>`${JSON.stringify(key)}:${canonical(record[key])}`).join(',')}}`;
17
+ }
18
+ return JSON.stringify(value);
19
+ }
20
+ function sha256Of(value) {
21
+ return createHash('sha256').update(canonical(value)).digest('hex');
22
+ }
23
+ function round6(value) {
24
+ return Number(value.toFixed(6));
25
+ }
26
+ export function loadPromptSet(path) {
27
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
28
+ if (raw['schema_version'] !== 'gotry_m3_nightly_prompt_set_v1') {
29
+ throw new Error(`prompt set schema_version must be gotry_m3_nightly_prompt_set_v1, got: ${String(raw['schema_version'])}`);
30
+ }
31
+ const turns = raw['turns'];
32
+ if (!Array.isArray(turns) || turns.length === 0 || turns.some((t)=>typeof t !== 'string' || t.length === 0)) {
33
+ throw new Error('prompt set turns must be a non-empty array of non-empty strings');
34
+ }
35
+ return {
36
+ schema_version: 'gotry_m3_nightly_prompt_set_v1',
37
+ description: raw['description'],
38
+ turns: turns
39
+ };
40
+ }
41
+ function assertPriceEntry(label, value) {
42
+ const entry = typeof value === 'object' && value !== null ? value : null;
43
+ if (!entry) throw new Error(`${label} must be an object`);
44
+ for (const field of [
45
+ 'input_cache_miss_usd_per_1m_peak',
46
+ 'input_cache_hit_usd_per_1m_peak',
47
+ 'output_usd_per_1m_peak'
48
+ ]){
49
+ const num = entry[field];
50
+ if (typeof num !== 'number' || !Number.isFinite(num) || num < 0) {
51
+ throw new Error(`${label}.${field} must be a non-negative finite number`);
52
+ }
53
+ }
54
+ return value;
55
+ }
56
+ export function loadPriceTable(path) {
57
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
58
+ const schemaVersion = raw['schema_version'];
59
+ if (schemaVersion === 'gotry_llm_price_table_v2') {
60
+ return loadPriceTableV2(raw);
61
+ }
62
+ if (schemaVersion === 'gotry_llm_price_table_v1') {
63
+ return loadPriceTableV1(raw);
64
+ }
65
+ throw new Error(`price table schema_version must be gotry_llm_price_table_v1 or _v2, got: ${String(schemaVersion)}`);
66
+ }
67
+ function loadPriceTableV1(raw) {
68
+ const aliases = typeof raw['aliases'] === 'object' && raw['aliases'] !== null ? raw['aliases'] : {};
69
+ for (const [from, to] of Object.entries(aliases)){
70
+ if (typeof to !== 'string') throw new Error(`price table alias ${from} must map to a model name`);
71
+ }
72
+ const prices = {};
73
+ for (const [name, entry] of Object.entries(typeof raw['prices'] === 'object' && raw['prices'] !== null ? raw['prices'] : {})){
74
+ prices[name] = assertPriceEntry(`price table entry ${name}`, entry);
75
+ }
76
+ return {
77
+ schema_version: 'gotry_llm_price_table_v1',
78
+ aliases,
79
+ prices
80
+ };
81
+ }
82
+ function loadPriceTableV2(raw) {
83
+ const providersObj = typeof raw['providers'] === 'object' && raw['providers'] !== null ? raw['providers'] : null;
84
+ if (!providersObj) throw new Error('price table v2 requires providers object');
85
+ const aliases = {};
86
+ const prices = {};
87
+ for (const [providerId, rawProvider] of Object.entries(providersObj)){
88
+ const provider = assertProviderEntry(`price table provider ${providerId}`, rawProvider);
89
+ for (const [aliasName, target] of Object.entries(provider.aliases)){
90
+ if (typeof target !== 'string') throw new Error(`provider ${providerId} alias ${aliasName} must map to a model name`);
91
+ if (!provider.models[target]) {
92
+ throw new Error(`provider ${providerId} alias ${aliasName} -> ${target} has no entry; update data/llm-price-table.json via PR`);
93
+ }
94
+ aliases[aliasName] = target;
95
+ }
96
+ for (const [modelName, entry] of Object.entries(provider.models)){
97
+ const qualified = `${providerId}:${modelName}`;
98
+ const validated = assertPriceEntry(`price table provider ${providerId} model ${modelName}`, entry);
99
+ if (prices[qualified]) throw new Error(`duplicate price table key ${qualified}`);
100
+ prices[qualified] = validated;
101
+ if (prices[modelName]) throw new Error(`duplicate price table key ${modelName} (across providers, disambiguate via aliases or qualified lookup)`);
102
+ prices[modelName] = validated;
103
+ }
104
+ }
105
+ return {
106
+ schema_version: 'gotry_llm_price_table_v2',
107
+ aliases,
108
+ prices
109
+ };
110
+ }
111
+ function assertProviderEntry(label, value) {
112
+ const entry = typeof value === 'object' && value !== null ? value : null;
113
+ if (!entry) throw new Error(`${label} must be an object`);
114
+ const family = entry['family'];
115
+ if (typeof family !== 'string' || family.length === 0) throw new Error(`${label}.family must be a non-empty string`);
116
+ const strategy = entry['price_strategy'];
117
+ if (strategy !== 'tiered_peak_offpeak' && strategy !== 'flat_no_offpeak') {
118
+ throw new Error(`${label}.price_strategy must be 'tiered_peak_offpeak' or 'flat_no_offpeak', got: ${String(strategy)}`);
119
+ }
120
+ const aliases = typeof entry['aliases'] === 'object' && entry['aliases'] !== null ? entry['aliases'] : {};
121
+ const models = typeof entry['models'] === 'object' && entry['models'] !== null ? entry['models'] : {};
122
+ return {
123
+ family,
124
+ price_strategy: strategy,
125
+ api_base: typeof entry['api_base'] === 'string' ? entry['api_base'] : undefined,
126
+ source_url: typeof entry['source_url'] === 'string' ? entry['source_url'] : undefined,
127
+ aliases,
128
+ models
129
+ };
130
+ }
131
+ export function priceRunCost(model, usage, table) {
132
+ if (usage.responsesMissingUsage > 0) {
133
+ throw new Error(`usage provider omitted usage on ${usage.responsesMissingUsage} response(s) — cost unprovable, refusing to price (fail-closed)`);
134
+ }
135
+ const alias = table.aliases[model];
136
+ if (alias && !table.prices[alias]) throw new Error(`price table alias ${model} -> ${alias} has no entry; update data/llm-price-table.json via PR`);
137
+ const entry = table.prices[alias ?? model];
138
+ if (!entry) throw new Error(`price table has no entry for model ${model}; update data/llm-price-table.json via PR (fail-closed, no guessed price)`);
139
+ const cost = (usage.inputCacheMissTokens * entry.input_cache_miss_usd_per_1m_peak + usage.inputCacheHitTokens * entry.input_cache_hit_usd_per_1m_peak + usage.outputTokens * entry.output_usd_per_1m_peak) / 1_000_000;
140
+ return round6(cost);
141
+ }
142
+ export function buildNightlyRunRecord(input) {
143
+ const { schema_version, executed_at, real_llm, prompt_set_sha256, output_sha256, cost_usd } = input;
144
+ const core = {
145
+ schema_version,
146
+ executed_at,
147
+ real_llm,
148
+ prompt_set_sha256,
149
+ output_sha256,
150
+ cost_usd
151
+ };
152
+ if (!SHA256.test(prompt_set_sha256) || !SHA256.test(output_sha256)) {
153
+ throw new Error('nightly record hashes must be lowercase SHA-256 digests');
154
+ }
155
+ const record = {
156
+ ...core,
157
+ run_key: `hmac-sha256:${createHash('sha256').update(canonical(core)).digest('hex')}`
158
+ };
159
+ parseNightlyRun(JSON.parse(JSON.stringify(record)), 0);
160
+ return record;
161
+ }
162
+ function activeModel() {
163
+ return process.env['LLM_MODEL'] ?? process.env['DEEPSEEK_MODEL'] ?? 'MiniMax-M2';
164
+ }
165
+ function arg(name) {
166
+ const index = process.argv.indexOf(name);
167
+ return index >= 0 ? process.argv[index + 1] : undefined;
168
+ }
169
+ export async function runNightlyEvidence(options) {
170
+ const clock = options.clock ?? (()=>new Date());
171
+ const llmKey = process.env['LLM_API_KEY'] ?? process.env['DEEPSEEK_API_KEY'];
172
+ if (!options.dryRun && !llmKey) {
173
+ return {
174
+ state: 'waiting_external_evidence',
175
+ record: null,
176
+ reason: 'no real LLM credential (LLM_API_KEY/DEEPSEEK_API_KEY); waiting & backoff & no-spend per issue #22 停机纪律'
177
+ };
178
+ }
179
+ const promptSet = loadPromptSet('data/m3-nightly-prompts.json');
180
+ const priceTable = loadPriceTable('data/llm-price-table.json');
181
+ const packPath = join('..', 'data', 'flights_2026.json');
182
+ const solvePort = realtimeSolvePort(solveUnified);
183
+ const port = options.dryRun ? createMockLlm(packPath) : createOpenAICompatLlm(packPath);
184
+ const state = newState();
185
+ const history = [];
186
+ for (const turn of promptSet.turns){
187
+ const { reply } = await runTurn(state, turn, port, [
188
+ ...history
189
+ ], solvePort);
190
+ history.push({
191
+ role: 'user',
192
+ text: turn
193
+ }, {
194
+ role: 'assistant',
195
+ text: reply
196
+ });
197
+ }
198
+ const promptSetSha = sha256Of(promptSet);
199
+ const turnsTranscript = promptSet.turns.map((turn, index)=>({
200
+ turn_index: index,
201
+ user: turn,
202
+ reply: history[index * 2 + 1]?.text ?? ''
203
+ }));
204
+ const outputSha = sha256Of(turnsTranscript);
205
+ let record;
206
+ if (options.dryRun) {
207
+ record = buildNightlyRunRecord({
208
+ schema_version: 'gotry_m3_nightly_run_v1',
209
+ executed_at: clock().toISOString(),
210
+ real_llm: false,
211
+ prompt_set_sha256: promptSetSha,
212
+ output_sha256: outputSha,
213
+ cost_usd: 0
214
+ });
215
+ return {
216
+ state: 'dry_run',
217
+ record,
218
+ reason: 'dry-run exercises the pipeline against mock LLM; no evidence written'
219
+ };
220
+ }
221
+ const usage = port.usage;
222
+ if (!usage) throw new Error('real LLM port did not expose usage tracker (dsh-llm regression) — refusing to write cost evidence');
223
+ if (usage.calls !== promptSet.turns.length) {
224
+ throw new Error(`usage calls ${usage.calls} != expected turns ${promptSet.turns.length} — fail-closed, no evidence written`);
225
+ }
226
+ const costUsd = priceRunCost(activeModel(), usage, priceTable);
227
+ record = buildNightlyRunRecord({
228
+ schema_version: 'gotry_m3_nightly_run_v1',
229
+ executed_at: clock().toISOString(),
230
+ real_llm: true,
231
+ prompt_set_sha256: promptSetSha,
232
+ output_sha256: outputSha,
233
+ cost_usd: costUsd
234
+ });
235
+ mkdirSync(options.evidenceRoot, {
236
+ recursive: true
237
+ });
238
+ appendFileSync(join(options.evidenceRoot, 'cohort.jsonl'), `${JSON.stringify(record)}\n`, 'utf8');
239
+ const budget = Number(process.env['GOTRY_NIGHTLY_BUDGET_USD'] ?? DEFAULT_BUDGET_USD);
240
+ const overBudget = Number.isFinite(budget) && budget > 0 ? costUsd > budget : false;
241
+ return {
242
+ state: 'evidence_written',
243
+ record,
244
+ cost_over_budget: overBudget,
245
+ reason: overBudget ? `cost_usd ${costUsd} exceeds GOTRY_NIGHTLY_BUDGET_USD ${budget} — backoff suggested` : undefined
246
+ };
247
+ }
248
+ function output(payload, asJson) {
249
+ if (asJson) console.log(JSON.stringify(payload));
250
+ else {
251
+ console.log(`nightly-evidence state: ${payload.state}${payload.error ? ` — ${payload.error}` : ''}`);
252
+ if (payload.record) console.log(JSON.stringify(payload.record, null, 2));
253
+ if (payload.reason) console.log(payload.reason);
254
+ }
255
+ }
256
+ async function main() {
257
+ if (!process.argv.includes('--no-env-file')) {
258
+ try {
259
+ for (const line of readFileSync(join('..', '.env'), 'utf-8').split('\n')){
260
+ const m = line.match(/^([A-Z_]+)=(.*)$/);
261
+ if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
262
+ }
263
+ } catch {}
264
+ }
265
+ const asJson = (arg('--format') ?? 'markdown') === 'json';
266
+ const evidenceRoot = arg('--evidence-root') ?? 'gotry-state/evidence/m3';
267
+ const dryRun = process.argv.includes('--dry-run');
268
+ try {
269
+ const result = await runNightlyEvidence({
270
+ evidenceRoot,
271
+ dryRun
272
+ });
273
+ output(result, asJson);
274
+ if (result.cost_over_budget) process.exitCode = 3;
275
+ } catch (error) {
276
+ output({
277
+ state: 'error',
278
+ record: null,
279
+ error: error.message
280
+ }, asJson);
281
+ process.exitCode = 1;
282
+ }
283
+ }
284
+ if (process.argv[1]?.endsWith('nightly-evidence.ts')) {
285
+ await main();
286
+ }
287
+
288
+
289
+ //# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/nightly-evidence.ts
@@ -80,4 +80,4 @@ if (channel === 'file') {
80
80
  }
81
81
 
82
82
 
83
- //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/nudge-digest.ts
83
+ //# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/nudge-digest.ts