humanish 0.48.0 → 0.49.0

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/dist/pricing.js CHANGED
@@ -12,6 +12,28 @@
12
12
  // so a reader can never confuse an estimate for an authoritative charge (invariant 6).
13
13
  export const PRICING_SCHEMA = "humanish.pricing.v1";
14
14
  export const ACTOR_ESTIMATED_COST_SCHEMA = "humanish.actor-estimated-cost.v1";
15
+ // The gpt-5.6 long-context tier: >272K input tokens re-prices the FULL request at 2x
16
+ // input-side / 1.5x output (developers.openai.com/api/docs/models/gpt-5.6-sol, 2026-08-18).
17
+ const GPT56_LONG_CONTEXT = {
18
+ thresholdInputTokens: 272_000,
19
+ inputMultiplier: 2,
20
+ outputMultiplier: 1.5
21
+ };
22
+ const GPT56_SOURCE = "developers.openai.com/api/docs/pricing (gpt-5.6 family, standard tier)";
23
+ // One 5.6-family entry: rates in USD-per-1M for legibility, converted once. Cache writes bill at
24
+ // 1.25x the uncached input rate on this family (`cache_write_tokens`, prompt-caching guide); the
25
+ // built-in `computer` tool has no per-call fee on the live sheet.
26
+ function gpt56Rate(inPer1M, cachedPer1M, writePer1M, outPer1M) {
27
+ return {
28
+ inputUsdPerToken: inPer1M * 1e-6,
29
+ cachedInputUsdPerToken: cachedPer1M * 1e-6,
30
+ cacheWriteUsdPerToken: writePer1M * 1e-6,
31
+ outputUsdPerToken: outPer1M * 1e-6,
32
+ longContext: { ...GPT56_LONG_CONTEXT },
33
+ asOf: "2026-08-18",
34
+ source: GPT56_SOURCE
35
+ };
36
+ }
15
37
  // Per-model rates, keyed on the model id that lands in trace.ids.model (lookup is
16
38
  // case-insensitive on a trimmed id). An id NOT present here is DECLARED ABSENT, never guessed.
17
39
  export const MODEL_RATES = {
@@ -23,17 +45,32 @@ export const MODEL_RATES = {
23
45
  asOf: "2026-08-01",
24
46
  source: "openai.com/api/pricing (computer-use-preview)"
25
47
  },
26
- // gpt-5.5 = the shipped CUA default (DEFAULT_OPENAI_CU_MODEL). $5 / 1M input, $30 / 1M output,
27
- // $0.50 / 1M cached input. NOTE: OpenAI's live pricing page no longer lists gpt-5.5 (superseded
28
- // by the gpt-5.6 family) — rate confirmed against public third-party sheets instead; refreshing
29
- // the default model is tracked in issue #334.
48
+ // gpt-5.5: the PREVIOUS-generation CUA default, kept so pinned labs and old bundles still
49
+ // price. Pre-5.6 models bill no cache-write fee (prompt-caching guide) and no long-context
50
+ // tier was published for it.
30
51
  "gpt-5.5": {
31
52
  inputUsdPerToken: 5e-6,
32
53
  outputUsdPerToken: 30e-6,
33
54
  cachedInputUsdPerToken: 0.5e-6,
34
55
  asOf: "2026-08-05",
35
56
  source: "openrouter.ai/openai/gpt-5.5 (gpt-5.5 no longer on openai.com/api/pricing; see #334)"
36
- }
57
+ },
58
+ // The gpt-5.6 family (live sheet 2026-08-18, standard tier, short-context base rates;
59
+ // the longContext block prices the >272K re-tier when per-request turns are recorded).
60
+ // sol = flagship (the shipped CUA default), terra = cost-balanced, luna = high-volume,
61
+ // cyber = the Daybreak frontier tier.
62
+ "gpt-5.6-sol": gpt56Rate(5, 0.5, 6.25, 30),
63
+ // "gpt-5.6" is OpenAI's own alias for gpt-5.6-sol (models index); priced identically so a
64
+ // lab configured with the alias never reads as unpriced.
65
+ "gpt-5.6": gpt56Rate(5, 0.5, 6.25, 30),
66
+ "gpt-5.6-terra": gpt56Rate(2, 0.2, 2.5, 12),
67
+ "gpt-5.6-luna": gpt56Rate(0.2, 0.02, 0.25, 1.2),
68
+ "gpt-5.6-cyber": gpt56Rate(12.5, 1.25, 15.625, 75),
69
+ // Daybreak program aliases (blue -> sol, red -> cyber today). OpenAI repoints these as new
70
+ // frontier models ship, so prefer the explicit tier id in labs; the entries exist so a
71
+ // configured alias still prices at what the alias bills TODAY.
72
+ "daybreak-blue-latest": gpt56Rate(5, 0.5, 6.25, 30),
73
+ "daybreak-red-latest": gpt56Rate(12.5, 1.25, 15.625, 75)
37
74
  };
38
75
  // E2B desktop sandbox compute, billed per-second by vCPU+RAM. Live sheet: 2 vCPU (default)
39
76
  // $0.000028/s + RAM $0.0000045/GiB/s => at an ASSUMED 4 GiB desktop, $0.000046/s ~= $0.00276/min
@@ -74,17 +111,70 @@ export function estimateActorCost(tokenUsage, modelId, rates = MODEL_RATES) {
74
111
  }
75
112
  const inTok = tokenUsage.input ?? 0;
76
113
  const outTok = tokenUsage.output ?? 0;
77
- // Cached input is billed at a fraction of the full rate, and on a session that threads state
78
- // through the provider it is the MAJORITY of input a warm prefix is re-sent every turn. Pricing
79
- // it at the full rate overstated real spend by up to ~10x on long sessions, which is enough to
80
- // abort a run against its own cap for money it never spent (#391).
81
- //
82
- // Both halves are honestly absent: no reported cachedInput, or a rate sheet without a cached
83
- // rate, prices exactly as before. We never assume a discount we cannot evidence.
84
- const cachedTok = rate.cachedInputUsdPerToken === undefined ? 0 : Math.min(inTok, Math.max(0, tokenUsage.cachedInput ?? 0));
85
- const fullTok = inTok - cachedTok;
86
- const inputUsd = round6(fullTok * rate.inputUsdPerToken + cachedTok * (rate.cachedInputUsdPerToken ?? 0));
87
- const outputUsd = round6(outTok * rate.outputUsdPerToken);
114
+ // Long-context tiering needs to know each REQUEST's input size (the provider re-prices whole
115
+ // requests past the threshold), so it engages only when per-turn usage records exist AND their
116
+ // input sums to the reported total a partial turn ledger must not silently price the missing
117
+ // remainder at the wrong tier. Otherwise totals price on the base (short-context) rate exactly
118
+ // as before, which is the under-estimate direction and never trips a cap early.
119
+ const turns = tokenUsage.turns ?? [];
120
+ const sumOf = (field) => turns.reduce((sum, turn) => sum + (turn[field] ?? 0), 0);
121
+ // The ledger is trusted only when it decomposes the totals EXACTLY — all four sums, not just
122
+ // input/output. A ledger that carries request sizes but not the cache splits would otherwise
123
+ // price every token at the full rate while the session totals sit ignored (red-team finding:
124
+ // 3.5-5x overstatement, the #391 false-cap-trip direction). Inconsistent evidence falls back
125
+ // to the totals path, which honors the declared splits on the base tier.
126
+ const tiered = rate.longContext !== undefined &&
127
+ turns.length > 0 &&
128
+ sumOf("input") === inTok &&
129
+ sumOf("output") === outTok &&
130
+ sumOf("cachedInput") === (tokenUsage.cachedInput ?? 0) &&
131
+ sumOf("cacheWriteInput") === (tokenUsage.cacheWriteInput ?? 0);
132
+ let inputUsd = 0;
133
+ let outputUsd = 0;
134
+ let cachedTotal = 0;
135
+ let writeTotal = 0;
136
+ let longContextTurns = 0;
137
+ // Price one request's usage at one tier. Cached input is billed at a fraction of the full rate,
138
+ // and on a session that threads provider state it is the MAJORITY of input — pricing it at the
139
+ // full rate overstated real spend by up to ~10x (#391). Cache WRITES bill at their own rate
140
+ // (1.25x on OpenAI 5.6+) as the total rate for those tokens; a sheet without a write rate
141
+ // prices writes as plain input (pre-5.6: writes are free-of-extra-fee, i.e. plain input).
142
+ // Every piece is honestly absent: no reported split means no discount and no surcharge assumed.
143
+ const priceRequest = (usage, tierable) => {
144
+ const reqIn = usage.input ?? 0;
145
+ const reqOut = usage.output ?? 0;
146
+ // Tiering applies only to a real per-REQUEST record: session totals crossing the threshold
147
+ // say nothing about any single request, so totals always price on the base tier.
148
+ const long = tierable && rate.longContext !== undefined && reqIn > rate.longContext.thresholdInputTokens;
149
+ const inMul = long ? rate.longContext.inputMultiplier : 1;
150
+ const outMul = long ? rate.longContext.outputMultiplier : 1;
151
+ if (long)
152
+ longContextTurns += 1;
153
+ const cached = rate.cachedInputUsdPerToken === undefined ? 0 : Math.min(reqIn, Math.max(0, usage.cachedInput ?? 0));
154
+ const writes = Math.min(reqIn - cached, Math.max(0, usage.cacheWriteInput ?? 0));
155
+ const full = reqIn - cached - writes;
156
+ cachedTotal += cached;
157
+ writeTotal += writes;
158
+ inputUsd +=
159
+ full * rate.inputUsdPerToken * inMul +
160
+ cached * (rate.cachedInputUsdPerToken ?? 0) * inMul +
161
+ writes * (rate.cacheWriteUsdPerToken ?? rate.inputUsdPerToken) * inMul;
162
+ outputUsd += reqOut * rate.outputUsdPerToken * outMul;
163
+ };
164
+ if (tiered) {
165
+ for (const turn of turns)
166
+ priceRequest(turn, true);
167
+ }
168
+ else {
169
+ priceRequest({
170
+ input: inTok,
171
+ output: outTok,
172
+ ...(tokenUsage.cachedInput === undefined ? {} : { cachedInput: tokenUsage.cachedInput }),
173
+ ...(tokenUsage.cacheWriteInput === undefined ? {} : { cacheWriteInput: tokenUsage.cacheWriteInput })
174
+ }, false);
175
+ }
176
+ inputUsd = round6(inputUsd);
177
+ outputUsd = round6(outputUsd);
88
178
  return {
89
179
  schema: ACTOR_ESTIMATED_COST_SCHEMA,
90
180
  estimatedCostUsd: round6(inputUsd + outputUsd),
@@ -97,7 +187,9 @@ export function estimateActorCost(tokenUsage, modelId, rates = MODEL_RATES) {
97
187
  outputUsd,
98
188
  inputTokens: inTok,
99
189
  outputTokens: outTok,
100
- ...(cachedTok > 0 ? { cachedInputTokens: cachedTok } : {})
190
+ ...(cachedTotal > 0 ? { cachedInputTokens: cachedTotal } : {}),
191
+ ...(writeTotal > 0 ? { cacheWriteInputTokens: writeTotal } : {}),
192
+ ...(longContextTurns > 0 ? { longContextTurns } : {})
101
193
  }
102
194
  };
103
195
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"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"}
package/dist/program.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Command } from "commander";
2
+ import { discoverProviderKeys } from "./key-resolution.js";
2
3
  import type { ObserverResult, ObserverServer } from "./observer.js";
3
4
  import type { OssMetaLabResult } from "./oss-meta-lab.js";
4
5
  export declare const CLI_RESPONSE_SCHEMA = "humanish.cli-response.v1";
@@ -15,7 +16,9 @@ export interface UnexpectedErrorEnvelope {
15
16
  message: string;
16
17
  };
17
18
  }
18
- export declare function createProgram(io?: Partial<CliIo>): Command;
19
+ export declare function createProgram(io?: Partial<CliIo> & {
20
+ keyDiscovery?: typeof discoverProviderKeys;
21
+ }): Command;
19
22
  /**
20
23
  * Default browser-open policy for a lab backend run. Mirrors the observe/watch gate:
21
24
  * an explicit --open/--no-open wins; --json (machine mode) never auto-opens; otherwise a
package/dist/program.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { Command, Option } from "commander";
7
7
  import { startCodexAppServerUi } from "./codex-app-server-ui.js";
8
8
  import { loadEnvFile } from "./env-file.js";
9
+ import { discoverProviderKeys, listUserKeys, resolveKeyName, setUserKey, unsetUserKey, userKeyStorePath } from "./key-resolution.js";
9
10
  import { redactText } from "./redaction.js";
10
11
  import { draftFeedback, listFeedback, renderIssueMarkdown, renderIssueUrl, verifyFeedback } from "./feedback.js";
11
12
  import { runInit } from "./init.js";
@@ -136,6 +137,7 @@ function reportUnexpectedActionError(command, io, error) {
136
137
  }
137
138
  export function createProgram(io = {}) {
138
139
  const cliIo = { ...defaultIo, ...io };
140
+ keyDiscoveryFn = io.keyDiscovery ?? discoverProviderKeys;
139
141
  const program = new HumanishCommand(undefined, cliIo);
140
142
  // Bare `humanish` orients instead of printing sixteen subcommands (#367). --help is untouched;
141
143
  // this is only what happens when no command was chosen at all.
@@ -177,6 +179,7 @@ export function createProgram(io = {}) {
177
179
  ].join("\n"));
178
180
  registerInitCommand(program, cliIo);
179
181
  registerDoctorCommand(program, cliIo);
182
+ registerKeysCommand(program, cliIo);
180
183
  registerRunCommand(program, cliIo);
181
184
  registerVerifyCommand(program, cliIo);
182
185
  registerCleanupCommand(program, cliIo);
@@ -234,6 +237,146 @@ function registerDoctorCommand(parent, io) {
234
237
  io.setExitCode(result.ok ? 0 : 2);
235
238
  });
236
239
  }
240
+ // The discovery fn the env seam calls; injectable via createProgram for hermetic CLI tests.
241
+ let keyDiscoveryFn = discoverProviderKeys;
242
+ const KEYS_RESULT_SCHEMA = "humanish.keys-result.v1";
243
+ function formatKeysHuman(result) {
244
+ const lines = [`humanish keys ${result.ok ? "ok" : "failed"}`, `store: ${result.store}`, result.message];
245
+ if (result.action === "list" && result.names.length > 0) {
246
+ for (const name of result.names)
247
+ lines.push(`- ${name}`);
248
+ }
249
+ return `${lines.join("\n")}\n`;
250
+ }
251
+ /** Read one secret line: from a piped stdin when --stdin, else a hidden TTY prompt. */
252
+ async function readSecretValue(useStdin, promptLabel, io) {
253
+ if (useStdin || !process.stdin.isTTY) {
254
+ const chunks = [];
255
+ for await (const chunk of process.stdin)
256
+ chunks.push(chunk);
257
+ const text = Buffer.concat(chunks).toString("utf8").trim();
258
+ return text.length > 0 ? text : null;
259
+ }
260
+ io.writeErr(`${promptLabel} (input hidden): `);
261
+ return await new Promise((resolve) => {
262
+ const stdin = process.stdin;
263
+ let value = "";
264
+ stdin.setRawMode?.(true);
265
+ stdin.resume();
266
+ stdin.setEncoding("utf8");
267
+ const onData = (key) => {
268
+ if (key === "\u0003") {
269
+ cleanup();
270
+ resolve(null);
271
+ return;
272
+ }
273
+ if (key === "\r" || key === "\n") {
274
+ cleanup();
275
+ io.writeErr("\n");
276
+ resolve(value.trim().length > 0 ? value.trim() : null);
277
+ return;
278
+ }
279
+ if (key === "\u007f" || key === "\b") {
280
+ value = value.slice(0, -1);
281
+ return;
282
+ }
283
+ value += key;
284
+ };
285
+ const cleanup = () => {
286
+ stdin.off("data", onData);
287
+ stdin.setRawMode?.(false);
288
+ stdin.pause();
289
+ };
290
+ stdin.on("data", onData);
291
+ });
292
+ }
293
+ function registerKeysCommand(parent, io) {
294
+ const keys = parent
295
+ .command("keys")
296
+ .description("Manage the humanish user-level key store used by provider-key discovery (#436).")
297
+ .summary("Manage the user-level provider key store.");
298
+ keys
299
+ .command("set")
300
+ .argument("<vendor-or-name>", "A vendor alias (openai, e2b, anthropic, github) or a raw ENV_NAME.")
301
+ .description("Store one provider key in the user store (0600), prompted with hidden input.")
302
+ .option("--stdin", "Read the value from stdin instead of prompting (for agents/pipes).")
303
+ .option("--json", JSON_OPTION_DESCRIPTION)
304
+ .action(async (vendorOrName, options, command) => {
305
+ const name = resolveKeyName(vendorOrName);
306
+ const storePath = userKeyStorePath(process.env);
307
+ if (name === null) {
308
+ const result = {
309
+ schema: KEYS_RESULT_SCHEMA, ok: false, action: "set", store: storePath, names: [],
310
+ message: `Not a vendor alias or valid env name: ${vendorOrName}. Vendors: openai, e2b, anthropic, github.`
311
+ };
312
+ writeResult(command, io, result, formatKeysHuman);
313
+ io.setExitCode(2);
314
+ return;
315
+ }
316
+ const value = await readSecretValue(options.stdin === true, `Value for ${name}`, io);
317
+ if (value === null) {
318
+ const result = {
319
+ schema: KEYS_RESULT_SCHEMA, ok: false, action: "set", store: storePath, names: [name],
320
+ message: "No value provided; nothing written."
321
+ };
322
+ writeResult(command, io, result, formatKeysHuman);
323
+ io.setExitCode(2);
324
+ return;
325
+ }
326
+ try {
327
+ const written = setUserKey(name, value, process.env);
328
+ const result = {
329
+ schema: KEYS_RESULT_SCHEMA, ok: true, action: "set", store: written.path, names: [name],
330
+ message: `${name} stored (0600). Live commands resolve it automatically; remove with "humanish keys unset ${name}".`
331
+ };
332
+ writeResult(command, io, result, formatKeysHuman);
333
+ io.setExitCode(0);
334
+ }
335
+ catch (error) {
336
+ const result = {
337
+ schema: KEYS_RESULT_SCHEMA, ok: false, action: "set", store: storePath, names: [name],
338
+ message: error instanceof Error ? error.message : "Failed to write the key store."
339
+ };
340
+ writeResult(command, io, result, formatKeysHuman);
341
+ io.setExitCode(2);
342
+ }
343
+ });
344
+ keys
345
+ .command("unset")
346
+ .argument("<vendor-or-name>", "A vendor alias or raw ENV_NAME to remove from the store.")
347
+ .description("Remove one key from the user store.")
348
+ .option("--json", JSON_OPTION_DESCRIPTION)
349
+ .action(async (vendorOrName, _options, command) => {
350
+ const name = resolveKeyName(vendorOrName);
351
+ const storePath = userKeyStorePath(process.env);
352
+ const had = name !== null && unsetUserKey(name, process.env);
353
+ const result = {
354
+ schema: KEYS_RESULT_SCHEMA, ok: name !== null, action: "unset", store: storePath,
355
+ names: name === null ? [] : [name],
356
+ message: name === null
357
+ ? `Not a vendor alias or valid env name: ${vendorOrName}.`
358
+ : had ? `${name} removed from the store.` : `${name} was not in the store; nothing changed.`
359
+ };
360
+ writeResult(command, io, result, formatKeysHuman);
361
+ io.setExitCode(name === null ? 2 : 0);
362
+ });
363
+ keys
364
+ .command("list")
365
+ .description("List the NAMES stored in the user store. Values are never printed.")
366
+ .option("--json", JSON_OPTION_DESCRIPTION)
367
+ .action(async (_options, command) => {
368
+ const storePath = userKeyStorePath(process.env);
369
+ const names = listUserKeys(process.env);
370
+ const result = {
371
+ schema: KEYS_RESULT_SCHEMA, ok: true, action: "list", store: storePath, names,
372
+ message: names.length === 0
373
+ ? "The store is empty. Add a key with `humanish keys set <vendor>`."
374
+ : `${names.length} key name(s) stored. Values are never printed.`
375
+ };
376
+ writeResult(command, io, result, formatKeysHuman);
377
+ io.setExitCode(0);
378
+ });
379
+ }
237
380
  function registerRunCommand(parent, io) {
238
381
  parent
239
382
  .command("run")
@@ -2440,16 +2583,29 @@ async function renderAndMaybeFollowObserver(args) {
2440
2583
  }
2441
2584
  }
2442
2585
  async function applyEnvFileOption(args) {
2443
- if (!args.envFile) {
2444
- return true;
2586
+ if (args.envFile) {
2587
+ const result = await loadEnvFile(args.cwd, args.envFile);
2588
+ if (!result.ok) {
2589
+ writeResult(args.command, args.io, result, formatEnvFileHuman);
2590
+ args.io.setExitCode(2);
2591
+ return false;
2592
+ }
2593
+ }
2594
+ // Provider-key discovery (#436): fill still-missing keys from the documented project
2595
+ // overlay, the owning vendors' native stores, and the humanish user store — fill-only
2596
+ // (an explicit --env-file or process env always wins), each fill announced by name and
2597
+ // source on stderr, never by value. HUMANISH_STRICT_KEYS=1 restores env-only behavior.
2598
+ try {
2599
+ await keyDiscoveryFn({
2600
+ cwd: args.cwd,
2601
+ env: process.env,
2602
+ announce: (line) => args.io.writeErr(`${line}\n`)
2603
+ });
2445
2604
  }
2446
- const result = await loadEnvFile(args.cwd, args.envFile);
2447
- if (result.ok) {
2448
- return true;
2605
+ catch {
2606
+ // Discovery must never break a command; a rung that fails to read is a miss, not an error.
2449
2607
  }
2450
- writeResult(args.command, args.io, result, formatEnvFileHuman);
2451
- args.io.setExitCode(2);
2452
- return false;
2608
+ return true;
2453
2609
  }
2454
2610
  function labReposOverride(options) {
2455
2611
  const override = [