humanish 0.141.0 → 0.142.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/ARCHITECTURE.md +1 -0
- package/CHANGELOG.md +11 -0
- package/README.md +2 -0
- package/dist/analysis/admission.d.ts +8 -0
- package/dist/analysis/admission.js +15 -4
- package/dist/analysis/automatic-config.d.ts +5 -2
- package/dist/analysis/automatic-config.js +23 -8
- package/dist/analysis/automatic.js +13 -7
- package/dist/analysis/execute.d.ts +17 -6
- package/dist/analysis/execute.js +36 -18
- package/dist/analysis/gemini-wire.d.ts +8 -0
- package/dist/analysis/gemini-wire.js +165 -0
- package/dist/analysis/image-tokens.d.ts +4 -2
- package/dist/analysis/image-tokens.js +24 -3
- package/dist/analysis/job.d.ts +1 -0
- package/dist/analysis/job.js +1 -0
- package/dist/analysis/openai-wire.d.ts +5 -0
- package/dist/analysis/openai-wire.js +101 -0
- package/dist/analysis/provider.d.ts +15 -7
- package/dist/analysis/provider.js +21 -101
- package/dist/analysis/responses.d.ts +3 -0
- package/dist/analysis/responses.js +19 -0
- package/dist/analysis/service.js +11 -3
- package/dist/analysis/types.d.ts +8 -2
- package/dist/analysis/validation.d.ts +1 -0
- package/dist/analysis/validation.js +13 -12
- package/dist/cli/commands/analyze.js +9 -7
- package/dist/cli/commands/observe.js +26 -1
- package/dist/cli/commands/run-command.d.ts +1 -1
- package/dist/cli/commands/run-command.js +12 -0
- package/dist/cli/commands/study-remote.d.ts +13 -0
- package/dist/cli/commands/study-remote.js +125 -0
- package/dist/cli/commands/study-route-computer-use.js +26 -2
- package/dist/cli/commands/study-route-scripted.js +1 -0
- package/dist/cli/commands/study-route-shared-world.js +1 -0
- package/dist/cli/commands/study-route-terminal.js +1 -0
- package/dist/cli/commands/study-run.js +34 -0
- package/dist/cli/commands/watch.js +5 -0
- package/dist/cli/doctor.js +7 -2
- package/dist/cli/findings.d.ts +8 -1
- package/dist/cli/findings.js +38 -15
- package/dist/cli/io.d.ts +8 -0
- package/dist/cli/io.js +2 -1
- package/dist/keys/key-resolution.d.ts +10 -0
- package/dist/keys/key-resolution.js +33 -5
- package/dist/observer/listen.d.ts +7 -2
- package/dist/observer/listen.js +10 -4
- package/dist/observer/render.d.ts +2 -0
- package/dist/observer/render.js +1 -1
- package/dist/observer-app.html +1 -1
- package/dist/remote/launch.d.ts +53 -0
- package/dist/remote/launch.js +320 -0
- package/dist/remote/observer-proxy.d.ts +11 -0
- package/dist/remote/observer-proxy.js +66 -0
- package/dist/remote/plan.d.ts +35 -0
- package/dist/remote/plan.js +114 -0
- package/dist/remote/reattach.d.ts +17 -0
- package/dist/remote/reattach.js +81 -0
- package/dist/remote/runner.d.ts +68 -0
- package/dist/remote/runner.js +216 -0
- package/dist/routes/computer-use/e2b-desktop/start.js +1 -1
- package/dist/routes/computer-use/types.d.ts +2 -0
- package/dist/routes/scripted/types.d.ts +2 -0
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/routes/terminal/types.d.ts +2 -0
- package/dist/run/costs.d.ts +2 -1
- package/dist/run/pricing.d.ts +13 -1
- package/dist/run/pricing.js +50 -4
- package/dist/run/results.d.ts +1 -1
- package/dist/run/run-cost.d.ts +2 -1
- package/dist/run/run-cost.js +6 -1
- package/dist/run/run.d.ts +13 -0
- package/dist/run/run.js +18 -7
- package/dist/run/sandbox-creates.d.ts +10 -3
- package/dist/run/sandbox-creates.js +48 -6
- package/dist/run-study.d.ts +5 -0
- package/dist/study/doctor.js +14 -1
- package/dist/study/requirements.d.ts +1 -1
- package/dist/study/requirements.js +8 -6
- package/dist/study/route-inputs.js +4 -0
- package/dist/study/run-study-events.d.ts +1 -1
- package/dist/study/run-study-events.js +9 -2
- package/dist/substrates/e2b/base-module.d.ts +3 -0
- package/dist/substrates/e2b/base-module.js +40 -0
- package/dist/substrates/e2b/desktop-browser.d.ts +1 -1
- package/dist/substrates/e2b/desktop-browser.js +24 -6
- package/dist/substrates/e2b/sdk.d.ts +24 -0
- package/dist/substrates/e2b/sdk.js +5 -1
- package/dist/substrates/e2b/shell-only.d.ts +5 -0
- package/dist/substrates/e2b/shell-only.js +27 -0
- package/dist/substrates/modal/sandbox.js +27 -28
- package/dist/substrates/modal/sdk.d.ts +7 -0
- package/dist/tui-app.js +46 -46
- package/docs/contracts/study-analysis.md +51 -3
- package/docs/product/automatic-analysis.md +26 -2
- package/package.json +2 -2
- package/skills/humanish/SKILL.md +10 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -86,6 +86,7 @@ the analysis of the run:
|
|
|
86
86
|
| `src/actors/` | The actor contract, the registry and each actor's session code | `src/actors/registry.ts` |
|
|
87
87
|
| `src/subject/` | Subject provisioning over a `Shell`: clone, local tree, desktop CLI, serve | `src/subject/serve.ts` |
|
|
88
88
|
| `src/substrates/` | Provider primitives: E2B in `e2b/`, Firecracker and Lima VMs in `local/`, the `Shell` | `src/substrates/desktop-session.ts` |
|
|
89
|
+
| `src/remote/` | `--remote`: plan a run for a runner sandbox, launch and follow it there, copy it back | `src/remote/launch.ts` |
|
|
89
90
|
| `src/run/` | Run lifecycle, bundle types, bundle guards, paths, status, receipts, reclaim | `src/run/run.ts` |
|
|
90
91
|
| `src/verify/` | `humanish verify`: evidence checks, share-safety grades, cost and rerun checks | `src/verify/verify.ts` |
|
|
91
92
|
| `src/evidence/` | Redaction, screenshot checks and desktop recordings | `src/evidence/redaction.ts` |
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,17 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.142.0: Remote runs, Gemini and GPT-6 analysis models, and a view-only live stream (2026-10-21)
|
|
12
|
+
|
|
13
|
+
humanish 0.142.0 can run a study remotely: `humanish run <study> --remote` puts the study's own
|
|
14
|
+
process in a sandbox, so a long study keeps going after this machine sleeps, and `observe --run
|
|
15
|
+
<id>` reconnects to it. Study analysis can run on `gpt-6.1-sol` and `gpt-6-luna` as well as the
|
|
16
|
+
default `gpt-6-astra`, and on Google's Gemini API with `humanish keys set gemini`. A hosted
|
|
17
|
+
desktop's live stream is now view-only on the VNC server itself: before, anyone holding a
|
|
18
|
+
participant's stream URL could type into that desktop.
|
|
19
|
+
|
|
20
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.142.0)
|
|
21
|
+
|
|
11
22
|
## 0.141.0: A run says when it is writing findings, and email links no longer open a dialog (2026-10-21)
|
|
12
23
|
|
|
13
24
|
humanish 0.141.0 says when a run is writing its findings: the CLI prints progress with the
|
package/README.md
CHANGED
|
@@ -242,6 +242,8 @@ beside Participants in Observer, with evidence links and separate participant
|
|
|
242
242
|
feedback. You can explicitly select a restricted Codex account analyst with
|
|
243
243
|
`npx humanish analyze --run latest --provider codex`; it uses remote inference,
|
|
244
244
|
requires the qualified CLI and account login, and reports dollar cost as unknown.
|
|
245
|
+
`npx humanish analyze --run latest --provider gemini --max-cost 2` sends the same
|
|
246
|
+
evidence to Google's Gemini API instead, with the key from `npx humanish keys set gemini`.
|
|
245
247
|
A study whose participant is a Codex `local-agent` uses that analyst by default. See the [analysis contract](docs/contracts/study-analysis.md) for
|
|
246
248
|
coverage limits, estimated cost controls, corrections and sharing behavior.
|
|
247
249
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { ModelRate } from "../run/pricing.js";
|
|
2
|
+
/**
|
|
3
|
+
* How many times that expectation an API analyst writes. The OpenAI figures above come from its
|
|
4
|
+
* billed analyses. A gemini-3.8-flash analysis of one participant at high thinking wrote 24,246
|
|
5
|
+
* output tokens against 13,000 expected, so Gemini's expectation is doubled. That rests on one run.
|
|
6
|
+
*/
|
|
7
|
+
export declare const EXPECTED_OUTPUT_SCALE: Readonly<Record<"openai" | "gemini", number>>;
|
|
2
8
|
/**
|
|
3
9
|
* When admission refuses a request, in words, for the cap as the sentence names it: `$3`, or `this`
|
|
4
10
|
* in the help of the option that sets it. Admission compares the smaller of the worst case and the
|
|
@@ -21,6 +27,8 @@ interface AnalysisRequestSize {
|
|
|
21
27
|
export interface AnalysisRequests {
|
|
22
28
|
cohorts: readonly AnalysisRequestSize[];
|
|
23
29
|
mergeTextBytes: number;
|
|
30
|
+
/** The analyst's EXPECTED_OUTPUT_SCALE; 1 when omitted. */
|
|
31
|
+
outputScale?: number;
|
|
24
32
|
}
|
|
25
33
|
export interface AnalysisCostEstimate {
|
|
26
34
|
inputTokens: number;
|
|
@@ -15,6 +15,15 @@ const FRAMING_TOKENS = 2048;
|
|
|
15
15
|
*/
|
|
16
16
|
const EXPECTED_OUTPUT_TOKENS = 12_000;
|
|
17
17
|
const EXPECTED_OUTPUT_TOKENS_PER_PARTICIPANT = 1_000;
|
|
18
|
+
/**
|
|
19
|
+
* How many times that expectation an API analyst writes. The OpenAI figures above come from its
|
|
20
|
+
* billed analyses. A gemini-3.8-flash analysis of one participant at high thinking wrote 24,246
|
|
21
|
+
* output tokens against 13,000 expected, so Gemini's expectation is doubled. That rests on one run.
|
|
22
|
+
*/
|
|
23
|
+
export const EXPECTED_OUTPUT_SCALE = {
|
|
24
|
+
openai: 1,
|
|
25
|
+
gemini: 2,
|
|
26
|
+
};
|
|
18
27
|
/** Admission compares the expected cost times this margin with the cap. */
|
|
19
28
|
const ADMISSION_MARGIN = 1.1;
|
|
20
29
|
const MARGIN_PERCENT = Math.round((ADMISSION_MARGIN - 1) * 100);
|
|
@@ -38,12 +47,13 @@ function requestCost(rate, inputTokens, outputTokens) {
|
|
|
38
47
|
outputTokens * rate.outputUsdPerToken * (long ? rate.longContext.outputMultiplier : 1));
|
|
39
48
|
}
|
|
40
49
|
/** One request's input and costs. `reportTokens` are earlier requests' reports it reads. */
|
|
41
|
-
function estimateRequest(rate, size, reportTokens = 0) {
|
|
50
|
+
function estimateRequest(rate, size, outputScale, reportTokens = 0) {
|
|
42
51
|
const inputTokens = Math.ceil(size.textBytes / BYTES_PER_INPUT_TOKEN) +
|
|
43
52
|
FRAMING_TOKENS +
|
|
44
53
|
size.imageTokens +
|
|
45
54
|
reportTokens;
|
|
46
|
-
const expectedOutputTokens = Math.min(size.outputAllowance,
|
|
55
|
+
const expectedOutputTokens = Math.min(size.outputAllowance, outputScale *
|
|
56
|
+
(EXPECTED_OUTPUT_TOKENS + EXPECTED_OUTPUT_TOKENS_PER_PARTICIPANT * size.participants));
|
|
47
57
|
return {
|
|
48
58
|
inputTokens,
|
|
49
59
|
expectedOutputTokens,
|
|
@@ -58,14 +68,15 @@ function estimateRequest(rate, size, reportTokens = 0) {
|
|
|
58
68
|
*/
|
|
59
69
|
export function estimateAnalysisCost(rate, requests) {
|
|
60
70
|
const { cohorts } = requests;
|
|
61
|
-
const
|
|
71
|
+
const scale = requests.outputScale ?? 1;
|
|
72
|
+
const estimates = cohorts.map((size) => estimateRequest(rate, size, scale));
|
|
62
73
|
if (cohorts.length > 1)
|
|
63
74
|
estimates.push(estimateRequest(rate, {
|
|
64
75
|
textBytes: requests.mergeTextBytes,
|
|
65
76
|
imageTokens: 0,
|
|
66
77
|
participants: cohorts.reduce((total, size) => total + size.participants, 0),
|
|
67
78
|
outputAllowance: Math.max(...cohorts.map((size) => size.outputAllowance)),
|
|
68
|
-
}, cohorts.reduce((total, size) => total + size.outputAllowance, 0)));
|
|
79
|
+
}, scale, cohorts.reduce((total, size) => total + size.outputAllowance, 0)));
|
|
69
80
|
const sum = (key) => estimates.reduce((total, estimate) => total + estimate[key], 0);
|
|
70
81
|
// Each request's cost is a whole number of micro-dollars, so rounding the sum to the nearest one
|
|
71
82
|
// only removes the addition's float noise.
|
|
@@ -2,6 +2,9 @@ import type { StudyRoute } from "../study/plan.js";
|
|
|
2
2
|
import type { AnalysisConfig } from "./types.js";
|
|
3
3
|
export declare const DEFAULT_ANALYSIS_TIMEOUT_MS = 600000;
|
|
4
4
|
export declare const DEFAULT_ANALYSIS_MODEL = "gpt-6-astra";
|
|
5
|
+
/** The model a Gemini analysis uses when the study or command names none: Google's current
|
|
6
|
+
* stable Flash model. */
|
|
7
|
+
export declare const DEFAULT_GEMINI_ANALYSIS_MODEL = "gemini-3.8-flash";
|
|
5
8
|
interface StudyAnalysisSettings {
|
|
6
9
|
model?: string;
|
|
7
10
|
question?: string;
|
|
@@ -9,7 +12,7 @@ interface StudyAnalysisSettings {
|
|
|
9
12
|
}
|
|
10
13
|
/** The study's review.analysis: a provider selection for a separate review after a live run. */
|
|
11
14
|
export type StudyAnalysis = StudyAnalysisSettings & ({
|
|
12
|
-
provider?: "openai";
|
|
15
|
+
provider?: "openai" | "gemini";
|
|
13
16
|
maxCostUsd: number;
|
|
14
17
|
maxOutputTokens?: number;
|
|
15
18
|
} | {
|
|
@@ -37,7 +40,7 @@ export declare function resolveAutomaticAnalysis(raw: unknown, participant?: Ana
|
|
|
37
40
|
message: string;
|
|
38
41
|
};
|
|
39
42
|
export interface AutomaticAnalysisBudget {
|
|
40
|
-
provider?: "openai" | "codex";
|
|
43
|
+
provider?: "openai" | "gemini" | "codex";
|
|
41
44
|
billing?: "api-estimate" | "account-unknown";
|
|
42
45
|
model: string;
|
|
43
46
|
maxCostUsd: number | null;
|
|
@@ -7,6 +7,9 @@ import { scanEncodedText } from "../evidence/encoded-text.js";
|
|
|
7
7
|
export const DEFAULT_ANALYSIS_TIMEOUT_MS = 600_000;
|
|
8
8
|
const MAX_ANALYSIS_TIMEOUT_MS = 600_000;
|
|
9
9
|
export const DEFAULT_ANALYSIS_MODEL = "gpt-6-astra";
|
|
10
|
+
/** The model a Gemini analysis uses when the study or command names none: Google's current
|
|
11
|
+
* stable Flash model. */
|
|
12
|
+
export const DEFAULT_GEMINI_ANALYSIS_MODEL = "gemini-3.8-flash";
|
|
10
13
|
const FIELDS = new Set([
|
|
11
14
|
"provider",
|
|
12
15
|
"maxCostUsd",
|
|
@@ -60,8 +63,11 @@ export function resolveAutomaticAnalysis(raw, participant) {
|
|
|
60
63
|
};
|
|
61
64
|
}
|
|
62
65
|
const value = raw;
|
|
63
|
-
if (value.provider !== undefined &&
|
|
64
|
-
|
|
66
|
+
if (value.provider !== undefined &&
|
|
67
|
+
value.provider !== "openai" &&
|
|
68
|
+
value.provider !== "gemini" &&
|
|
69
|
+
value.provider !== "codex") {
|
|
70
|
+
return { ok: false, message: "review.analysis.provider must be openai, gemini or codex." };
|
|
65
71
|
}
|
|
66
72
|
if (value.provider === "codex") {
|
|
67
73
|
const model = value.model === undefined ? CODEX_ANALYSIS_MODEL : value.model;
|
|
@@ -93,7 +99,12 @@ export function resolveAutomaticAnalysis(raw, participant) {
|
|
|
93
99
|
};
|
|
94
100
|
}
|
|
95
101
|
const { maxCostUsd } = value;
|
|
96
|
-
const
|
|
102
|
+
const vendor = value.provider === "gemini" ? "gemini" : "openai";
|
|
103
|
+
const model = value.model !== undefined
|
|
104
|
+
? value.model
|
|
105
|
+
: vendor === "gemini"
|
|
106
|
+
? DEFAULT_GEMINI_ANALYSIS_MODEL
|
|
107
|
+
: DEFAULT_ANALYSIS_MODEL;
|
|
97
108
|
const question = value.question === undefined ? null : value.question;
|
|
98
109
|
const timeoutMs = value.timeoutMs === undefined ? DEFAULT_ANALYSIS_TIMEOUT_MS : value.timeoutMs;
|
|
99
110
|
const maxOutputTokens = value.maxOutputTokens === undefined
|
|
@@ -104,7 +115,7 @@ export function resolveAutomaticAnalysis(raw, participant) {
|
|
|
104
115
|
maxCostUsd <= 0 ||
|
|
105
116
|
maxCostUsd > 1000 ||
|
|
106
117
|
typeof model !== "string" ||
|
|
107
|
-
!isSupportedAnalysisModel(model) ||
|
|
118
|
+
!isSupportedAnalysisModel(model, vendor) ||
|
|
108
119
|
!validTimeout(timeoutMs) ||
|
|
109
120
|
typeof maxOutputTokens !== "number" ||
|
|
110
121
|
!Number.isSafeInteger(maxOutputTokens) ||
|
|
@@ -113,7 +124,7 @@ export function resolveAutomaticAnalysis(raw, participant) {
|
|
|
113
124
|
!validQuestion(value.question)) {
|
|
114
125
|
return {
|
|
115
126
|
ok: false,
|
|
116
|
-
message: `review.analysis requires a positive maxCostUsd up to 1000, a supported analysis model, timeoutMs 1–${MAX_ANALYSIS_TIMEOUT_MS}, maxOutputTokens 256–${MAX_ANALYSIS_OUTPUT_TOKENS}, and an optional question of at most 4000 characters.`,
|
|
127
|
+
message: `review.analysis requires a positive maxCostUsd up to 1000, a supported ${vendor === "gemini" ? "Gemini" : "OpenAI"} analysis model, timeoutMs 1–${MAX_ANALYSIS_TIMEOUT_MS}, maxOutputTokens 256–${MAX_ANALYSIS_OUTPUT_TOKENS}, and an optional question of at most 4000 characters.`,
|
|
117
128
|
};
|
|
118
129
|
}
|
|
119
130
|
if (question !== null && scanEncodedText(question).sensitive)
|
|
@@ -121,7 +132,8 @@ export function resolveAutomaticAnalysis(raw, participant) {
|
|
|
121
132
|
return {
|
|
122
133
|
ok: true,
|
|
123
134
|
config: {
|
|
124
|
-
|
|
135
|
+
// An omitted provider stays omitted: it is the OpenAI contract, and its hash predates the field.
|
|
136
|
+
...(value.provider === undefined ? {} : { provider: vendor }),
|
|
125
137
|
model,
|
|
126
138
|
maxCostUsd,
|
|
127
139
|
timeoutMs,
|
|
@@ -148,7 +160,9 @@ export function automaticAnalysisBudget(raw, route, participants, participant) {
|
|
|
148
160
|
return {
|
|
149
161
|
...(config.provider === "codex"
|
|
150
162
|
? { provider: "codex", billing: "account-unknown" }
|
|
151
|
-
:
|
|
163
|
+
: config.provider === "gemini"
|
|
164
|
+
? { provider: "gemini" }
|
|
165
|
+
: {}),
|
|
152
166
|
model: config.model,
|
|
153
167
|
maxCostUsd: config.maxCostUsd,
|
|
154
168
|
trigger: raw === undefined ? "default" : "explicit",
|
|
@@ -179,5 +193,6 @@ export function formatAutomaticAnalysisBudget(budget) {
|
|
|
179
193
|
const refused = budget.refusedFromUsd === undefined || who === undefined
|
|
180
194
|
? `refused before it starts if ${rule}`
|
|
181
195
|
: `refused before it starts for ${who} even with no evidence (expected $${budget.refusedFromUsd.toFixed(2)}), since ${rule}`;
|
|
182
|
-
|
|
196
|
+
const where = budget.provider === "gemini" ? " on Gemini, which sends the evidence to Google" : "";
|
|
197
|
+
return `After live runs: ${budget.trigger} analysis${where} · ${budget.model}${range} · ${refused}; this is not a billing cap. Set review.analysis: false to disable.`;
|
|
183
198
|
}
|
|
@@ -3,6 +3,7 @@ import { resolveRunPath } from "../run/locate.js";
|
|
|
3
3
|
import { refreshObserver, analyzeRun, readCompletedAnalysisSource, resolveAnalysisRun, } from "./service.js";
|
|
4
4
|
import { preferLargerAnalysisOutput, ANALYSIS_PROMPT_VERSION } from "./execute.js";
|
|
5
5
|
import { captureEvidence } from "./evidence.js";
|
|
6
|
+
import { analysisApiKey } from "../keys/key-resolution.js";
|
|
6
7
|
import { loadRunTraces } from "../run/stream-trace.js";
|
|
7
8
|
import { hashAnalysisValue } from "./validation.js";
|
|
8
9
|
import { refusedCost } from "./admission.js";
|
|
@@ -56,7 +57,11 @@ function statusReason(result) {
|
|
|
56
57
|
return "AUTOMATIC_ANALYSIS_CANCELLED";
|
|
57
58
|
return null;
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
+
/** The skip reason for a missing API key, which names the key the selected analyst reads. */
|
|
61
|
+
const keyMissingReason = (config) => config.provider === "gemini"
|
|
62
|
+
? "AUTOMATIC_ANALYSIS_GEMINI_KEY_MISSING"
|
|
63
|
+
: "AUTOMATIC_ANALYSIS_KEY_MISSING";
|
|
64
|
+
function outcomeOf(result, config) {
|
|
60
65
|
if (result.error?.code === "ANALYSIS_PUBLICATION_FAILED")
|
|
61
66
|
return { state: "failed", reason: "AUTOMATIC_ANALYSIS_PUBLICATION_FAILED", result };
|
|
62
67
|
if (result.error?.code?.startsWith("analysis_codex_"))
|
|
@@ -67,7 +72,7 @@ function outcomeOf(result) {
|
|
|
67
72
|
if (code === "ANALYSIS_CANCELLED")
|
|
68
73
|
return { state: "cancelled", reason: "AUTOMATIC_ANALYSIS_CANCELLED", result };
|
|
69
74
|
if (code === "ANALYSIS_API_KEY_MISSING")
|
|
70
|
-
return { ...skipped(
|
|
75
|
+
return { ...skipped(keyMissingReason(config)), result };
|
|
71
76
|
if (code === "ANALYSIS_BUSY")
|
|
72
77
|
return { ...skipped("AUTOMATIC_ANALYSIS_BUSY"), result };
|
|
73
78
|
if (code === "ANALYSIS_HISTORY_UNAVAILABLE")
|
|
@@ -158,7 +163,10 @@ export async function runAutomaticAnalysis(cwdInput, runId, configInput, deps =
|
|
|
158
163
|
if (!prepared)
|
|
159
164
|
return skipped("AUTOMATIC_ANALYSIS_SOURCE_UNAVAILABLE");
|
|
160
165
|
cwd = physicalCwdOf(prepared);
|
|
161
|
-
const hasKey = Boolean((deps.apiKey ??
|
|
166
|
+
const hasKey = Boolean((deps.apiKey ??
|
|
167
|
+
(configInput.provider === "codex"
|
|
168
|
+
? undefined
|
|
169
|
+
: analysisApiKey(configInput.provider ?? "openai", process.env)))?.trim());
|
|
162
170
|
const source = await readAutomaticSource(cwd, prepared, structuredClone(configInput), deps, hasKey);
|
|
163
171
|
if ("state" in source)
|
|
164
172
|
return source;
|
|
@@ -225,9 +233,7 @@ export async function runAutomaticAnalysis(cwdInput, runId, configInput, deps =
|
|
|
225
233
|
else if (deps.defaultRequest === true && (missingKey || !participantEvidence)) {
|
|
226
234
|
outcome = signal.aborted
|
|
227
235
|
? { state: "cancelled", reason: "AUTOMATIC_ANALYSIS_CANCELLED" }
|
|
228
|
-
: skipped(missingKey
|
|
229
|
-
? "AUTOMATIC_ANALYSIS_KEY_MISSING"
|
|
230
|
-
: "AUTOMATIC_ANALYSIS_NO_PARTICIPANT_EVIDENCE");
|
|
236
|
+
: skipped(missingKey ? keyMissingReason(config) : "AUTOMATIC_ANALYSIS_NO_PARTICIPANT_EVIDENCE");
|
|
231
237
|
}
|
|
232
238
|
else {
|
|
233
239
|
const result = await analyzeRun(cwd, runId, { config }, {
|
|
@@ -250,7 +256,7 @@ export async function runAutomaticAnalysis(cwdInput, runId, configInput, deps =
|
|
|
250
256
|
});
|
|
251
257
|
},
|
|
252
258
|
});
|
|
253
|
-
outcome = outcomeOf(result);
|
|
259
|
+
outcome = outcomeOf(result, config);
|
|
254
260
|
}
|
|
255
261
|
if (cancellationFailed)
|
|
256
262
|
outcome = {
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { type AnalysisArtifact, type AnalysisConfig, type AnalysisInput } from "./types.js";
|
|
2
|
-
import { type AnalysisFetch, type AnalysisProvider } from "./provider.js";
|
|
2
|
+
import { type AnalysisApiVendor, type AnalysisFetch, type AnalysisProvider } from "./provider.js";
|
|
3
3
|
import type { RejectedAnalysisOutput } from "./diagnostics.js";
|
|
4
4
|
export declare const ANALYSIS_PROMPT_VERSION = "study-evidence-8";
|
|
5
|
-
export declare const
|
|
6
|
-
/**
|
|
5
|
+
export declare const SUPPORTED_ANALYSIS_MODELS: readonly string[];
|
|
6
|
+
/**
|
|
7
|
+
* Gemini models that completed a live analysis. Each model's page under
|
|
8
|
+
* ai.google.dev/gemini-api/docs/models (read 2026-10-21) also lists image input and JSON-schema
|
|
9
|
+
* output for gemini-3.5-flash-lite, gemini-3-flash-preview and gemini-4-argon; they join this list
|
|
10
|
+
* once a live analysis on each completes.
|
|
11
|
+
*/
|
|
12
|
+
export declare const GEMINI_ANALYSIS_MODELS: readonly string[];
|
|
13
|
+
/** Whether the API analyst `provider` (omitted is OpenAI) analyzes with `model`. */
|
|
14
|
+
export declare const isSupportedAnalysisModel: (model: string, provider?: AnalysisApiVendor) => boolean;
|
|
15
|
+
/** The API output limit when a study omits maxOutputTokens, and the most it may ask for. Gemini 4
|
|
16
|
+
* returns at most 32,768 output tokens from one generateContent request. */
|
|
7
17
|
export declare const DEFAULT_ANALYSIS_MAX_OUTPUT_TOKENS = 16384;
|
|
8
18
|
export declare const MAX_ANALYSIS_OUTPUT_TOKENS = 32768;
|
|
9
19
|
export interface AnalysisAdmission {
|
|
@@ -38,9 +48,10 @@ export interface AnalysisProgress {
|
|
|
38
48
|
* Local admission estimate, not a provider-enforced billed-spend guarantee. No token-count API
|
|
39
49
|
* call, credential, or network access. admission.ts holds the cost model: text at a calibrated
|
|
40
50
|
* bytes-per-token ratio, the expected output, and the worst case that spends the whole output
|
|
41
|
-
* allowance.
|
|
42
|
-
* formula
|
|
43
|
-
*
|
|
51
|
+
* allowance. image-tokens.ts prices each image: on OpenAI from its PNG size by the vision guide's
|
|
52
|
+
* patch formula, or at the 3,000-token ceiling when its size or the model's sizing is unknown; on
|
|
53
|
+
* Gemini at the fixed high-resolution count. The rates are those of the day admission runs.
|
|
54
|
+
* Unknown models/rates fail closed rather than inheriting those assumptions.
|
|
44
55
|
* Known-sensitive decoded text denies admission with analysis_input_sensitive or
|
|
45
56
|
* analysis_question_sensitive. Neither error includes rejected input values.
|
|
46
57
|
*/
|
package/dist/analysis/execute.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { validCodexAnalysisConfig } from "./codex-config.js";
|
|
2
|
-
import {
|
|
3
|
-
import { estimateAnalysisCost } from "./admission.js";
|
|
2
|
+
import { INPUT_IMAGE_DATA_URL, highDetailImageTokens, imageTokenCeiling } from "./image-tokens.js";
|
|
3
|
+
import { EXPECTED_OUTPUT_SCALE, estimateAnalysisCost } from "./admission.js";
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
-
import {
|
|
5
|
+
import { modelRate } from "../run/pricing.js";
|
|
6
6
|
import { containsSensitive } from "../evidence/redaction.js";
|
|
7
7
|
import { scanEncodedText } from "../evidence/encoded-text.js";
|
|
8
8
|
import { ANALYSIS_ID_PATTERN, ANALYSIS_SCHEMA, } from "./types.js";
|
|
9
|
-
import {
|
|
9
|
+
import { createAnalysisProvider, } from "./provider.js";
|
|
10
10
|
import { hashAnalysisValue, analysisResultJsonSchema, validateAnalysisInputMetadata, } from "./validation.js";
|
|
11
11
|
import { analysisCohorts, analysisRequestCount, EVIDENCE_LIMITS } from "./analysis-limits.js";
|
|
12
12
|
import { checkMergedResponse, cohortInputs, mergePacket, mergeResultJsonSchema, } from "./cohorts.js";
|
|
13
|
-
import { checkProviderAnalysis, codexWarnings, recordProviderUsage, } from "./responses.js";
|
|
13
|
+
import { checkProviderAnalysis, codexWarnings, httpStatusWarning, recordProviderUsage, } from "./responses.js";
|
|
14
14
|
import { mapWithConcurrency } from "../run/concurrency.js";
|
|
15
15
|
export const ANALYSIS_PROMPT_VERSION = "study-evidence-8";
|
|
16
|
-
const SUPPORTED_ANALYSIS_MODELS = Object.freeze([
|
|
16
|
+
export const SUPPORTED_ANALYSIS_MODELS = Object.freeze([
|
|
17
17
|
"gpt-6-astra",
|
|
18
|
+
"gpt-6.1-sol",
|
|
19
|
+
"gpt-6-luna",
|
|
18
20
|
"gpt-5.5",
|
|
19
21
|
"gpt-5.6",
|
|
20
22
|
"gpt-5.6-sol",
|
|
@@ -22,8 +24,18 @@ const SUPPORTED_ANALYSIS_MODELS = Object.freeze([
|
|
|
22
24
|
"gpt-5.6-luna",
|
|
23
25
|
]);
|
|
24
26
|
const SUPPORTED_MODELS = new Set(SUPPORTED_ANALYSIS_MODELS);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Gemini models that completed a live analysis. Each model's page under
|
|
29
|
+
* ai.google.dev/gemini-api/docs/models (read 2026-10-21) also lists image input and JSON-schema
|
|
30
|
+
* output for gemini-3.5-flash-lite, gemini-3-flash-preview and gemini-4-argon; they join this list
|
|
31
|
+
* once a live analysis on each completes.
|
|
32
|
+
*/
|
|
33
|
+
export const GEMINI_ANALYSIS_MODELS = Object.freeze(["gemini-3.8-flash"]);
|
|
34
|
+
const GEMINI_MODELS = new Set(GEMINI_ANALYSIS_MODELS);
|
|
35
|
+
/** Whether the API analyst `provider` (omitted is OpenAI) analyzes with `model`. */
|
|
36
|
+
export const isSupportedAnalysisModel = (model, provider = "openai") => (provider === "gemini" ? GEMINI_MODELS : SUPPORTED_MODELS).has(model);
|
|
37
|
+
/** The API output limit when a study omits maxOutputTokens, and the most it may ask for. Gemini 4
|
|
38
|
+
* returns at most 32,768 output tokens from one generateContent request. */
|
|
27
39
|
export const DEFAULT_ANALYSIS_MAX_OUTPUT_TOKENS = 16_384;
|
|
28
40
|
export const MAX_ANALYSIS_OUTPUT_TOKENS = 32_768;
|
|
29
41
|
const MAX_EVIDENCE_BYTES = 1024 * 1024;
|
|
@@ -154,9 +166,10 @@ function inputError(input) {
|
|
|
154
166
|
* Local admission estimate, not a provider-enforced billed-spend guarantee. No token-count API
|
|
155
167
|
* call, credential, or network access. admission.ts holds the cost model: text at a calibrated
|
|
156
168
|
* bytes-per-token ratio, the expected output, and the worst case that spends the whole output
|
|
157
|
-
* allowance.
|
|
158
|
-
* formula
|
|
159
|
-
*
|
|
169
|
+
* allowance. image-tokens.ts prices each image: on OpenAI from its PNG size by the vision guide's
|
|
170
|
+
* patch formula, or at the 3,000-token ceiling when its size or the model's sizing is unknown; on
|
|
171
|
+
* Gemini at the fixed high-resolution count. The rates are those of the day admission runs.
|
|
172
|
+
* Unknown models/rates fail closed rather than inheriting those assumptions.
|
|
160
173
|
* Known-sensitive decoded text denies admission with analysis_input_sensitive or
|
|
161
174
|
* analysis_question_sensitive. Neither error includes rejected input values.
|
|
162
175
|
*/
|
|
@@ -175,8 +188,10 @@ export function estimateAnalysisAdmission(input, config) {
|
|
|
175
188
|
});
|
|
176
189
|
if ((config.provider === "codex"
|
|
177
190
|
? !validCodexAnalysisConfig(config)
|
|
178
|
-
: (config.provider !== undefined &&
|
|
179
|
-
|
|
191
|
+
: (config.provider !== undefined &&
|
|
192
|
+
config.provider !== "openai" &&
|
|
193
|
+
config.provider !== "gemini") ||
|
|
194
|
+
!isSupportedAnalysisModel(config.model, config.provider) ||
|
|
180
195
|
!Number.isFinite(config.maxCostUsd) ||
|
|
181
196
|
config.maxCostUsd <= 0 ||
|
|
182
197
|
config.maxCostUsd > 1000 ||
|
|
@@ -209,7 +224,7 @@ export function estimateAnalysisAdmission(input, config) {
|
|
|
209
224
|
ratesAsOf: null,
|
|
210
225
|
requests,
|
|
211
226
|
};
|
|
212
|
-
const rate =
|
|
227
|
+
const rate = modelRate(config.model);
|
|
213
228
|
if (!rate ||
|
|
214
229
|
rate.placeholder ||
|
|
215
230
|
!Number.isFinite(rate.inputUsdPerToken) ||
|
|
@@ -230,6 +245,7 @@ export function estimateAnalysisAdmission(input, config) {
|
|
|
230
245
|
}),
|
|
231
246
|
// Without reports: estimateAnalysisCost prices them at the cohorts' output allowances.
|
|
232
247
|
mergeTextBytes: requestBytes(mergeRequest(input, config, [])),
|
|
248
|
+
outputScale: EXPECTED_OUTPUT_SCALE[config.provider ?? "openai"],
|
|
233
249
|
});
|
|
234
250
|
if (!Number.isFinite(cost.worstCaseCostUsd))
|
|
235
251
|
return denied("analysis_rate_unknown");
|
|
@@ -284,7 +300,7 @@ const RANGE_BISECTIONS = 40;
|
|
|
284
300
|
export function analysisCostRange(config, participants, preferLargerOutput = false) {
|
|
285
301
|
if (config.provider === "codex")
|
|
286
302
|
return undefined;
|
|
287
|
-
const rate =
|
|
303
|
+
const rate = modelRate(config.model);
|
|
288
304
|
if (!rate || rate.placeholder)
|
|
289
305
|
return undefined;
|
|
290
306
|
const covered = Math.min(participants, EVIDENCE_LIMITS.participants);
|
|
@@ -306,9 +322,10 @@ export function analysisCostRange(config, participants, preferLargerOutput = fal
|
|
|
306
322
|
outputAllowance,
|
|
307
323
|
textBytes: cohortPrompt +
|
|
308
324
|
share * (EVIDENCE_LIMITS.textBytes + EVIDENCE_LIMITS.evidence * ENTRY_STRUCTURE_BYTES),
|
|
309
|
-
imageTokens: share * EVIDENCE_LIMITS.captures *
|
|
325
|
+
imageTokens: share * EVIDENCE_LIMITS.captures * imageTokenCeiling(config.model),
|
|
310
326
|
})),
|
|
311
327
|
mergeTextBytes: mergePrompt + share * covered * ENTRY_STRUCTURE_BYTES,
|
|
328
|
+
outputScale: EXPECTED_OUTPUT_SCALE[config.provider ?? "openai"],
|
|
312
329
|
});
|
|
313
330
|
const admits = (share, allowance) => request(share, allowance).admittedCostUsd <= config.maxCostUsd;
|
|
314
331
|
// Cost grows with the share, so admission admits each allowance up to one share of the limits.
|
|
@@ -415,6 +432,7 @@ async function analysisProvider(config, options) {
|
|
|
415
432
|
cliVersion: config.identity.cliVersion,
|
|
416
433
|
}))
|
|
417
434
|
: createAnalysisProvider({
|
|
435
|
+
vendor: config.provider ?? "openai",
|
|
418
436
|
apiKey: options.apiKey,
|
|
419
437
|
...(options.fetch === undefined ? {} : { fetchFn: options.fetch }),
|
|
420
438
|
});
|
|
@@ -534,8 +552,8 @@ export async function runAnalysis(input, config, options) {
|
|
|
534
552
|
});
|
|
535
553
|
responses.push(response);
|
|
536
554
|
recordProviderUsage(artifact, config, responses);
|
|
537
|
-
for (const warning of codexWarnings(config, response))
|
|
538
|
-
if (!options.warnings?.includes(warning))
|
|
555
|
+
for (const warning of [...codexWarnings(config, response), httpStatusWarning(config, response)])
|
|
556
|
+
if (warning !== undefined && !options.warnings?.includes(warning))
|
|
539
557
|
options.warnings?.push(warning);
|
|
540
558
|
if (response.status !== "completed")
|
|
541
559
|
return {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AnalysisWire } from "./provider.js";
|
|
2
|
+
/**
|
|
3
|
+
* One stateless request: no tools, high thinking, JSON constrained to the schema, the standard
|
|
4
|
+
* service tier, and `store: false` so the request stays out of the project's request logs. Each
|
|
5
|
+
* capture goes at MEDIA_RESOLUTION_HIGH, the setting the media-resolution guide recommends for
|
|
6
|
+
* image analysis, after a text part naming its evidence key.
|
|
7
|
+
*/
|
|
8
|
+
export declare const geminiWire: AnalysisWire;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// The Gemini API generateContent wire format of one analysis request
|
|
2
|
+
// (ai.google.dev/api/generate-content, read 2026-10-21): what the request body says and how a
|
|
3
|
+
// response becomes an AnalysisProviderResult. provider.ts owns the transport around it.
|
|
4
|
+
import { isRecord } from "../run/type-guards.js";
|
|
5
|
+
import { INPUT_IMAGE_DATA_URL } from "./image-tokens.js";
|
|
6
|
+
const record = (value) => (isRecord(value) ? value : {});
|
|
7
|
+
const count = (value) => Number.isSafeInteger(value) && value >= 0 && value <= 1e12;
|
|
8
|
+
/**
|
|
9
|
+
* The response schema as Gemini accepts it, and the array limits it leaves out. Gemini refuses the
|
|
10
|
+
* analysis schemas with a 400 while they carry `maxItems`: its constrained decoder counts the
|
|
11
|
+
* states each nested array limit adds, and its structured-output guide names large nested
|
|
12
|
+
* `maxItems` as a cause of that refusal. So any Gemini model can return an array past its limit;
|
|
13
|
+
* validation.ts checks every response against the full schema and refuses that one as
|
|
14
|
+
* analysis_validation_failed_schema_invalid. The limits go to the model as instructions instead.
|
|
15
|
+
*/
|
|
16
|
+
function withoutArrayLimits(schema) {
|
|
17
|
+
const limits = [];
|
|
18
|
+
const strip = (value, path) => {
|
|
19
|
+
if (Array.isArray(value))
|
|
20
|
+
return value.map((item) => strip(item, path));
|
|
21
|
+
if (!isRecord(value))
|
|
22
|
+
return value;
|
|
23
|
+
if (typeof value.maxItems === "number")
|
|
24
|
+
limits.push(`${path || "the answer"}: ${value.maxItems}`);
|
|
25
|
+
return Object.fromEntries(Object.entries(value)
|
|
26
|
+
.filter(([key]) => key !== "maxItems")
|
|
27
|
+
.map(([key, child]) => [
|
|
28
|
+
key,
|
|
29
|
+
key === "properties" && isRecord(child)
|
|
30
|
+
? Object.fromEntries(Object.entries(child).map(([name, field]) => [
|
|
31
|
+
name,
|
|
32
|
+
strip(field, path ? `${path}.${name}` : name),
|
|
33
|
+
]))
|
|
34
|
+
: strip(child, key === "items" ? `${path}[]` : path),
|
|
35
|
+
]));
|
|
36
|
+
};
|
|
37
|
+
return { schema: strip(schema, ""), limits };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Proto3 JSON leaves out a count that is zero, so an absent thoughts, candidates or cached count is
|
|
41
|
+
* 0. promptTokenCount is never zero for a request that carries evidence, so it must be present.
|
|
42
|
+
* Thoughts bill as output (ai.google.dev/gemini-api/docs/pricing: "Output price (including
|
|
43
|
+
* thinking tokens)"). totalTokenCount must equal the parts, or none of them is trusted.
|
|
44
|
+
*/
|
|
45
|
+
function usageOf(raw) {
|
|
46
|
+
const usage = record(record(raw).usageMetadata);
|
|
47
|
+
const counted = (key) => {
|
|
48
|
+
const value = usage[key] ?? 0;
|
|
49
|
+
return count(value) ? value : null;
|
|
50
|
+
};
|
|
51
|
+
const prompt = usage.promptTokenCount;
|
|
52
|
+
const toolPrompt = counted("toolUsePromptTokenCount");
|
|
53
|
+
const candidates = counted("candidatesTokenCount");
|
|
54
|
+
const thoughts = counted("thoughtsTokenCount");
|
|
55
|
+
const cached = counted("cachedContentTokenCount");
|
|
56
|
+
if (!count(prompt) ||
|
|
57
|
+
toolPrompt === null ||
|
|
58
|
+
candidates === null ||
|
|
59
|
+
thoughts === null ||
|
|
60
|
+
cached === null)
|
|
61
|
+
return null;
|
|
62
|
+
const input = prompt + toolPrompt;
|
|
63
|
+
const output = candidates + thoughts;
|
|
64
|
+
if (cached > input ||
|
|
65
|
+
(usage.totalTokenCount !== undefined && usage.totalTokenCount !== input + output))
|
|
66
|
+
return null;
|
|
67
|
+
return { input, output, ...(cached > 0 ? { cachedInput: cached } : {}) };
|
|
68
|
+
}
|
|
69
|
+
/** The finish reasons that end a candidate without a usable answer, by what they mean. The
|
|
70
|
+
* provider declined to answer: its safety, recitation, language, blocklist or policy filters. */
|
|
71
|
+
const DECLINED = new Set([
|
|
72
|
+
"SAFETY",
|
|
73
|
+
"RECITATION",
|
|
74
|
+
"LANGUAGE",
|
|
75
|
+
"BLOCKLIST",
|
|
76
|
+
"PROHIBITED_CONTENT",
|
|
77
|
+
"SPII",
|
|
78
|
+
"IMAGE_SAFETY",
|
|
79
|
+
"IMAGE_PROHIBITED_CONTENT",
|
|
80
|
+
"IMAGE_RECITATION",
|
|
81
|
+
"ESCALATION",
|
|
82
|
+
"PUP_LIMITED_DISABLED",
|
|
83
|
+
]);
|
|
84
|
+
/** The answer stopped at the output limit, or at a long-decode hop this one request never
|
|
85
|
+
* follows. */
|
|
86
|
+
const CUT_OFF = new Set(["MAX_TOKENS", "CONTINUATION"]);
|
|
87
|
+
/** One candidate whose only non-thought part is the JSON answer; anything else fails closed. */
|
|
88
|
+
function parseGenerateContent(raw) {
|
|
89
|
+
const root = record(raw);
|
|
90
|
+
const base = { output: null, usage: usageOf(raw), dispatched: true };
|
|
91
|
+
if (record(root.promptFeedback).blockReason !== undefined)
|
|
92
|
+
return { ...base, status: "refused", errorCode: "refusal" };
|
|
93
|
+
const candidates = Array.isArray(root.candidates) ? root.candidates : [];
|
|
94
|
+
if (candidates.length !== 1)
|
|
95
|
+
return { ...base, status: "failed", errorCode: "invalid_response" };
|
|
96
|
+
const candidate = record(candidates[0]);
|
|
97
|
+
const reason = String(candidate.finishReason);
|
|
98
|
+
if (DECLINED.has(reason))
|
|
99
|
+
return { ...base, status: "refused", errorCode: "refusal" };
|
|
100
|
+
if (CUT_OFF.has(reason))
|
|
101
|
+
return { ...base, status: "incomplete", errorCode: "output_incomplete" };
|
|
102
|
+
const parts = record(candidate.content).parts;
|
|
103
|
+
// A thought summary is never part of the answer; thoughtSignature on a text part is metadata.
|
|
104
|
+
const answer = Array.isArray(parts) ? parts.filter((part) => record(part).thought !== true) : [];
|
|
105
|
+
const text = record(answer[0]).text;
|
|
106
|
+
if (reason !== "STOP" ||
|
|
107
|
+
answer.length !== 1 ||
|
|
108
|
+
typeof text !== "string" ||
|
|
109
|
+
Object.keys(record(answer[0])).some((key) => key !== "text" && key !== "thoughtSignature"))
|
|
110
|
+
return { ...base, status: "failed", errorCode: "invalid_response" };
|
|
111
|
+
try {
|
|
112
|
+
return { ...base, status: "completed", output: JSON.parse(text), errorCode: null };
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return { ...base, status: "failed", errorCode: "invalid_response" };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* One stateless request: no tools, high thinking, JSON constrained to the schema, the standard
|
|
120
|
+
* service tier, and `store: false` so the request stays out of the project's request logs. Each
|
|
121
|
+
* capture goes at MEDIA_RESOLUTION_HIGH, the setting the media-resolution guide recommends for
|
|
122
|
+
* image analysis, after a text part naming its evidence key.
|
|
123
|
+
*/
|
|
124
|
+
export const geminiWire = {
|
|
125
|
+
url: (model) => `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
|
|
126
|
+
headers: (apiKey) => ({ "x-goog-api-key": apiKey, "Content-Type": "application/json" }),
|
|
127
|
+
body: (request) => {
|
|
128
|
+
const { schema, limits } = withoutArrayLimits(request.schema);
|
|
129
|
+
const limitText = limits.length === 0
|
|
130
|
+
? ""
|
|
131
|
+
: `\n\nThe response schema sent with this request omits its array limits. Keep each array at or under its limit, by field path: ${limits.join("; ")}.`;
|
|
132
|
+
return {
|
|
133
|
+
systemInstruction: { parts: [{ text: request.instructions + limitText }] },
|
|
134
|
+
contents: [
|
|
135
|
+
{
|
|
136
|
+
// The API leaves role optional outside a multi-turn conversation.
|
|
137
|
+
parts: [
|
|
138
|
+
{ text: request.evidence },
|
|
139
|
+
...request.images.flatMap((image) => {
|
|
140
|
+
const [, type, data] = INPUT_IMAGE_DATA_URL.exec(image.dataUrl);
|
|
141
|
+
return [
|
|
142
|
+
{ text: JSON.stringify({ captureEvidenceId: image.evidenceId }) },
|
|
143
|
+
{
|
|
144
|
+
inlineData: { mimeType: `image/${type}`, data },
|
|
145
|
+
mediaResolution: { level: "MEDIA_RESOLUTION_HIGH" },
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
}),
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
generationConfig: {
|
|
153
|
+
candidateCount: 1,
|
|
154
|
+
maxOutputTokens: request.maxOutputTokens,
|
|
155
|
+
thinkingConfig: { thinkingLevel: "HIGH" },
|
|
156
|
+
responseFormat: {
|
|
157
|
+
text: { mimeType: "APPLICATION_JSON", schema },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
serviceTier: "standard",
|
|
161
|
+
store: false,
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
parse: parseGenerateContent,
|
|
165
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export declare const
|
|
1
|
+
/** An input image the analysis providers accept; the groups are the image type and its base64 bytes. */
|
|
2
|
+
export declare const INPUT_IMAGE_DATA_URL: RegExp;
|
|
3
|
+
/** The most one image can cost on `model`, whatever its size. */
|
|
4
|
+
export declare const imageTokenCeiling: (model: string) => number;
|
|
3
5
|
export declare function highDetailImageTokens(model: string, dataUrl: string): number;
|