@gr8ful/spf 0.18.0 → 0.19.1
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 +8 -0
- package/assets/skill/references/config.md +1 -0
- package/dist/cli/commands/doctor.js +25 -3
- package/dist/cli/commands/estimate.d.ts +22 -6
- package/dist/cli/commands/estimate.js +32 -10
- package/dist/cli/commands/loop.d.ts +20 -0
- package/dist/cli/commands/loop.js +20 -1
- package/dist/cli/commands/ui.js +2 -1
- package/dist/cli/commands/watch.js +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/interview.js +13 -0
- package/dist/cli/ui/run_dashboard.js +13 -7
- package/dist/core/agent_cc.d.ts +19 -3
- package/dist/core/agent_cc.js +38 -18
- package/dist/core/agent_flue.js +51 -14
- package/dist/core/agent_opencode.d.ts +62 -25
- package/dist/core/agent_opencode.js +71 -30
- package/dist/core/agents.d.ts +51 -4
- package/dist/core/agents.js +79 -4
- package/dist/core/console.d.ts +24 -4
- package/dist/core/console.js +20 -7
- package/dist/core/data_types.d.ts +300 -19
- package/dist/core/data_types.js +134 -5
- package/dist/core/issues/jira_provider.d.ts +58 -3
- package/dist/core/issues/jira_provider.js +89 -18
- package/dist/core/issues/markdown_adf.d.ts +58 -0
- package/dist/core/issues/markdown_adf.js +705 -0
- package/dist/core/issues/provider.d.ts +23 -0
- package/dist/core/loop.d.ts +39 -1
- package/dist/core/loop.js +33 -2
- package/dist/core/ollama_provider.d.ts +96 -13
- package/dist/core/ollama_provider.js +172 -26
- package/dist/core/otel.js +10 -1
- package/dist/core/otel_propagation.d.ts +168 -24
- package/dist/core/otel_propagation.js +219 -43
- package/dist/core/permissions.d.ts +16 -1
- package/dist/core/permissions.js +91 -3
- package/dist/core/providers.js +8 -3
- package/dist/core/refine.js +13 -1
- package/dist/core/runner.d.ts +33 -2
- package/dist/core/runner.js +40 -5
- package/dist/core/tiering.js +7 -3
- package/dist/core/tracer.d.ts +7 -1
- package/dist/core/tracer.js +15 -3
- package/dist/ui/server/db.d.ts +8 -1
- package/dist/ui/server/db.js +21 -4
- package/dist/ui/server/serve.d.ts +7 -0
- package/dist/ui/server/serve.js +10 -7
- package/dist/ui/shared/types.d.ts +16 -0
- package/package.json +3 -3
|
@@ -311,6 +311,29 @@ export interface IssueAuthoringProvider {
|
|
|
311
311
|
* roll-up, refine cannot function without authoring at all.
|
|
312
312
|
*/
|
|
313
313
|
listChildren(parent: Issue): Promise<Issue[]>;
|
|
314
|
+
/**
|
|
315
|
+
* Best-effort: relate a freshly published tree's ROOT issue back to the
|
|
316
|
+
* spec issue (`specId`) it was refined FROM — `core/refine.ts`'s
|
|
317
|
+
* `publish()` calls this once per root node (a node with no `parent` of
|
|
318
|
+
* its own within the refined tree) when it was given a `specIssueId`.
|
|
319
|
+
*
|
|
320
|
+
* Deliberately NOT `linkChild`: that method sets the tracker's
|
|
321
|
+
* HIERARCHY field (Jira's `parent`, GitHub's sub-issues API), and a
|
|
322
|
+
* spec's own issue type (Story by default — `JiraIssueTypeMapSchema.spec`)
|
|
323
|
+
* frequently cannot legally PARENT a root node's type in Jira's
|
|
324
|
+
* issue-type hierarchy — see `JiraProvider`'s own doc comment on
|
|
325
|
+
* `publishSpecs()` for the same constraint. `linkToSpec` uses a plain,
|
|
326
|
+
* symmetric issue-to-issue reference instead (Jira's generic "issue
|
|
327
|
+
* link"), which has no such hierarchy restriction.
|
|
328
|
+
*
|
|
329
|
+
* OPTIONAL, not every tracker needs one: GitHub already gets a native,
|
|
330
|
+
* visible cross-reference for free the moment `renderBody`'s "## Parent"
|
|
331
|
+
* section renders a plain "#N" in the body — GitHub auto-links same-repo
|
|
332
|
+
* issue mentions into a real timeline "referenced this issue" event, no
|
|
333
|
+
* API call required. Jira does not do this for plain text, which is why
|
|
334
|
+
* `JiraProvider` implements this and `GitHubProvider` does not.
|
|
335
|
+
*/
|
|
336
|
+
linkToSpec?(specId: string, issue: Issue): Promise<void>;
|
|
314
337
|
}
|
|
315
338
|
/**
|
|
316
339
|
* Structural, not nominal: checks for the three methods rather than
|
package/dist/core/loop.d.ts
CHANGED
|
@@ -106,7 +106,35 @@ export interface LedgerAttempt {
|
|
|
106
106
|
error: string | null;
|
|
107
107
|
/** HEAD's short sha after a successful (exit 0) iteration — unchanged from the previous attempt's if this iteration committed nothing new, which is exactly what `isStuck` below looks for. `null` only for an errored or non-accepted (nonzero exit) attempt, where nothing was checked out to read. */
|
|
108
108
|
commit_sha: string | null;
|
|
109
|
+
/**
|
|
110
|
+
* Kept for backward compatibility with ledger rows written before
|
|
111
|
+
* `billable_tokens` (below) existed — on a row this old, `tokens` is the
|
|
112
|
+
* DISPLAY total (`sessions.total_tokens`, cache reads included), the only
|
|
113
|
+
* figure that era ever recorded. On every row written since, this field
|
|
114
|
+
* and `billable_tokens` are set to the SAME (billable) value; they only
|
|
115
|
+
* ever diverge across that one-time boundary, never within a single
|
|
116
|
+
* write. `cumulativeSpend` is what actually reads either — see its own
|
|
117
|
+
* doc comment for the precedence.
|
|
118
|
+
*/
|
|
109
119
|
tokens: number;
|
|
120
|
+
/**
|
|
121
|
+
* The BILLABLE figure (input + cache-write + output) — what
|
|
122
|
+
* `--max-tokens`/`overCumulativeBudget` is SUPPOSED to compare against,
|
|
123
|
+
* same metric `defaults.max_run_tokens` uses within one chain run (see
|
|
124
|
+
* `UsageBreakdown.billable_tokens`'s doc comment, `data_types.ts`). NOT
|
|
125
|
+
* the display total (`sessions.total_tokens`, cache reads included) —
|
|
126
|
+
* `cli/commands/loop.ts`'s readback reads `session.billable_tokens`,
|
|
127
|
+
* falling back to `total_tokens` only for a SESSION row that predates
|
|
128
|
+
* that sqlite column being populated.
|
|
129
|
+
*
|
|
130
|
+
* `undefined` on any LEDGER row written before this field existed — a
|
|
131
|
+
* genuinely old-format row, not a free ($0) iteration. `cumulativeSpend`
|
|
132
|
+
* falls back to `tokens` for exactly that row (the only figure it has,
|
|
133
|
+
* a display total it cannot retroactively convert), and
|
|
134
|
+
* `hasLegacyTokenRows` is what a caller uses to warn once that a ceiling
|
|
135
|
+
* check is now mixing metrics across the ledger's own history.
|
|
136
|
+
*/
|
|
137
|
+
billable_tokens?: number;
|
|
110
138
|
cost: number;
|
|
111
139
|
failures: string[];
|
|
112
140
|
started_at: string;
|
|
@@ -151,13 +179,22 @@ export declare function newLedger(input: {
|
|
|
151
179
|
export declare function resolveGoalId(explicit: string | undefined): string;
|
|
152
180
|
export interface CumulativeBudget {
|
|
153
181
|
maxCost?: number;
|
|
182
|
+
/** `--max-tokens` — checked against BILLABLE tokens (see `LedgerAttempt.billable_tokens`'s own doc comment), the same metric `defaults.max_run_tokens` checks per-call inside one chain run. */
|
|
154
183
|
maxTokens?: number;
|
|
155
184
|
}
|
|
156
185
|
export declare function cumulativeSpend(attempts: LedgerAttempt[]): {
|
|
157
186
|
cost: number;
|
|
158
187
|
tokens: number;
|
|
159
188
|
};
|
|
160
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* True when the ledger holds at least one attempt written before
|
|
191
|
+
* `billable_tokens` existed — `cumulativeSpend` is then silently mixing a
|
|
192
|
+
* display-total figure (that row's `tokens`) into a sum whose newer rows
|
|
193
|
+
* are billable. Used to print a one-time operator warning rather than let
|
|
194
|
+
* that mismatch pass unremarked; see `runLoop`'s call site.
|
|
195
|
+
*/
|
|
196
|
+
export declare function hasLegacyTokenRows(attempts: LedgerAttempt[]): boolean;
|
|
197
|
+
/** Whether the goal-scoped ceiling is already exhausted going into the NEXT iteration — a stronger, ledger-wide check than any single iteration's own budget. `spend.tokens` must already be BILLABLE tokens (see `LedgerAttempt.tokens`) — this function just compares, it does not know which metric it was handed. */
|
|
161
198
|
export declare function overCumulativeBudget(budget: CumulativeBudget, spend: {
|
|
162
199
|
cost: number;
|
|
163
200
|
tokens: number;
|
|
@@ -185,6 +222,7 @@ export interface IterationResult {
|
|
|
185
222
|
error: string | null;
|
|
186
223
|
/** `null` when nothing was committed this iteration (a no-op, or a throw before any commit). */
|
|
187
224
|
commit_sha: string | null;
|
|
225
|
+
/** BILLABLE tokens — see `LedgerAttempt.tokens`'s own doc comment; `runIteration` (`cli/commands/loop.ts`) reads this straight from `LedgerAttempt`, so the two must stay the same metric. */
|
|
188
226
|
tokens: number;
|
|
189
227
|
cost: number;
|
|
190
228
|
/** `null` when the iteration errored or exited non-zero — the stop check only ever runs against a chain that accepted its own work, same as `fixLoop` only re-verifies after a phase that didn't already throw. */
|
package/dist/core/loop.js
CHANGED
|
@@ -132,10 +132,30 @@ export function newLedger(input) {
|
|
|
132
132
|
export function resolveGoalId(explicit) {
|
|
133
133
|
return explicit ?? newId(8);
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Per-attempt token figure for budget purposes: `billable_tokens` where the
|
|
137
|
+
* row has it, else `tokens` (an old-format row, predating that field — see
|
|
138
|
+
* `LedgerAttempt.billable_tokens`'s own doc comment). Never the other way
|
|
139
|
+
* around: a new row's `tokens` happens to equal its `billable_tokens` today,
|
|
140
|
+
* but `billable_tokens` is the one this function trusts on purpose.
|
|
141
|
+
*/
|
|
142
|
+
function tokensForBudget(a) {
|
|
143
|
+
return a.billable_tokens ?? a.tokens;
|
|
144
|
+
}
|
|
135
145
|
export function cumulativeSpend(attempts) {
|
|
136
|
-
return attempts.reduce((acc, a) => ({ cost: acc.cost + a.cost, tokens: acc.tokens + a
|
|
146
|
+
return attempts.reduce((acc, a) => ({ cost: acc.cost + a.cost, tokens: acc.tokens + tokensForBudget(a) }), { cost: 0, tokens: 0 });
|
|
137
147
|
}
|
|
138
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* True when the ledger holds at least one attempt written before
|
|
150
|
+
* `billable_tokens` existed — `cumulativeSpend` is then silently mixing a
|
|
151
|
+
* display-total figure (that row's `tokens`) into a sum whose newer rows
|
|
152
|
+
* are billable. Used to print a one-time operator warning rather than let
|
|
153
|
+
* that mismatch pass unremarked; see `runLoop`'s call site.
|
|
154
|
+
*/
|
|
155
|
+
export function hasLegacyTokenRows(attempts) {
|
|
156
|
+
return attempts.some((a) => a.billable_tokens === undefined);
|
|
157
|
+
}
|
|
158
|
+
/** Whether the goal-scoped ceiling is already exhausted going into the NEXT iteration — a stronger, ledger-wide check than any single iteration's own budget. `spend.tokens` must already be BILLABLE tokens (see `LedgerAttempt.tokens`) — this function just compares, it does not know which metric it was handed. */
|
|
139
159
|
export function overCumulativeBudget(budget, spend) {
|
|
140
160
|
if (budget.maxCost !== undefined && spend.cost >= budget.maxCost)
|
|
141
161
|
return true;
|
|
@@ -207,6 +227,13 @@ export async function runLoop(deps) {
|
|
|
207
227
|
if (existing) {
|
|
208
228
|
deps.log(`loop: resuming goal ${deps.goalId} — ${existing.attempts.length} attempt(s) already recorded`);
|
|
209
229
|
}
|
|
230
|
+
// One-time info line, not a per-iteration one: legacy rows already IN the
|
|
231
|
+
// ledger (from a resumed goal) do not change count as this run proceeds,
|
|
232
|
+
// so there is nothing to re-warn about after the first check.
|
|
233
|
+
if (deps.budget.maxTokens !== undefined && hasLegacyTokenRows(ledger.attempts)) {
|
|
234
|
+
deps.log(`loop: goal ${deps.goalId} — ledger has attempt(s) recorded before billable-token tracking; ` +
|
|
235
|
+
"--max-tokens falls back to their display-total token count for those rows (see LedgerAttempt.billable_tokens)");
|
|
236
|
+
}
|
|
210
237
|
let lastVerdict = ledger.attempts.length > 0 ? { passed: false, failures: ledger.attempts[ledger.attempts.length - 1].failures, artifacts: [] } : null;
|
|
211
238
|
for (let i = ledger.attempts.length + 1; i <= deps.max; i++) {
|
|
212
239
|
if (overCumulativeBudget(deps.budget, cumulativeSpend(ledger.attempts))) {
|
|
@@ -244,7 +271,11 @@ export async function runLoop(deps) {
|
|
|
244
271
|
exit_code: result.exit_code,
|
|
245
272
|
error: result.error,
|
|
246
273
|
commit_sha: result.commit_sha,
|
|
274
|
+
// Every row written from here on sets both to the same (billable)
|
|
275
|
+
// value — see `LedgerAttempt.tokens`/`billable_tokens`'s doc comments
|
|
276
|
+
// for why they only ever diverge on an old-format row.
|
|
247
277
|
tokens: result.tokens,
|
|
278
|
+
billable_tokens: result.tokens,
|
|
248
279
|
cost: result.cost,
|
|
249
280
|
failures,
|
|
250
281
|
started_at: startedAt,
|
|
@@ -42,28 +42,111 @@
|
|
|
42
42
|
* cost nothing at runtime either way.
|
|
43
43
|
*/
|
|
44
44
|
import type { Provider } from "@earendil-works/pi-ai";
|
|
45
|
+
/**
|
|
46
|
+
* `OLLAMA_API_KEY`, trimmed, when the operator has set one — e.g. the
|
|
47
|
+
* Briefs gateway (Envoy AI Gateway) enforces a per-client bearer and 401s
|
|
48
|
+
* the dummy key `DUMMY_API_KEY` was designed for a bare local server that
|
|
49
|
+
* checks nothing. Falls back to the dummy exactly as before when unset, so
|
|
50
|
+
* THIS function's own return value — the resolved `Authorization` bearer —
|
|
51
|
+
* is byte-identical to pre-gateway behavior for a bare local Ollama server
|
|
52
|
+
* (the common case this module was built for). MINOR-H: that is narrower
|
|
53
|
+
* than "the whole request is unchanged" — it is not, even with
|
|
54
|
+
* `OLLAMA_API_KEY` unset: a `traceparent` (and, once `agent_flue.ts` has an
|
|
55
|
+
* adw_id/agent_name to give it, `x-correlation-id`/`x-spf-agent`) is ALWAYS
|
|
56
|
+
* sent now, gateway or no gateway (see the "Gateway headers" section below).
|
|
57
|
+
* A bare local Ollama server ignores headers it doesn't recognize, so this
|
|
58
|
+
* is harmless — just not byte-identical. Read fresh inside `resolve()` (see
|
|
59
|
+
* its call site below) — never cached — so a key exported mid-process (or
|
|
60
|
+
* changed) takes effect on the very next dispatch with no re-registration.
|
|
61
|
+
*
|
|
62
|
+
* MINOR 3: exported so `doctor.ts`'s `OLLAMA_BASE_URL reachability` probe
|
|
63
|
+
* calls this SAME function rather than reading `process.env.OLLAMA_API_KEY`
|
|
64
|
+
* raw — a prior version of that probe sent NO `Authorization` header at all
|
|
65
|
+
* when the env var was unset, which diverges from what a real dispatch
|
|
66
|
+
* sends (the dummy bearer below, always). Against a gateway that rejects a
|
|
67
|
+
* request with no `Authorization` header at all differently than one with a
|
|
68
|
+
* wrong/dummy bearer, that divergence could make doctor report reachable
|
|
69
|
+
* when a real dispatch would 401, or vice versa. Calling `ollamaApiKey()` in
|
|
70
|
+
* both places means doctor's probe and a real dispatch send byte-identical
|
|
71
|
+
* bearers for the same env state.
|
|
72
|
+
*/
|
|
73
|
+
export declare function ollamaApiKey(): string;
|
|
74
|
+
/** SPF-side identity for one LLM call, as far as `registerOllamaModel`'s caller can supply it. Both fields optional — an absent one simply omits its header. */
|
|
75
|
+
export interface GatewayCallContext {
|
|
76
|
+
/** The run's adw_id — sent as `x-correlation-id` so the gateway groups this run's calls. */
|
|
77
|
+
adwId?: string;
|
|
78
|
+
/** The SPF agent name — sent as `x-spf-agent`. */
|
|
79
|
+
agentName?: string;
|
|
80
|
+
}
|
|
81
|
+
/** Must NEVER be sent — Envoy/Switchyard own it end-to-end; a client-supplied value breaks their sampling. Exported only so tests can assert its absence by name, not a literal string. The sole surviving export of this name in the codebase — see MINOR-G in this change's review; `otel_propagation.ts` no longer has one now that `XRequestIdPropagator` is gone (BLOCKER B). */
|
|
82
|
+
export declare const X_REQUEST_ID_HEADER = "x-request-id";
|
|
83
|
+
/**
|
|
84
|
+
* A fresh W3C `traceparent` for one outbound call — used only on the NOT
|
|
85
|
+
* INSTALLED path (see the section above); when propagation IS installed,
|
|
86
|
+
* `resolve()` does not call this at all, relying entirely on the
|
|
87
|
+
* instrumentation's own per-request injection instead (this is what fixed
|
|
88
|
+
* BLOCKER A/MAJOR-C: this function used to be called unconditionally, and
|
|
89
|
+
* on the installed path it silently reused the one still-open span's
|
|
90
|
+
* traceparent for every call inside that span, which is both a duplicate
|
|
91
|
+
* header AND not actually fresh per call).
|
|
92
|
+
*
|
|
93
|
+
* Reuses the active OTel span context when one is installed and current —
|
|
94
|
+
* the same trace this call's other telemetry already belongs to — falling
|
|
95
|
+
* back to a brand-new random trace/span id pair when there is none (no span
|
|
96
|
+
* active at this exact point, e.g. a stray call before any span opened), so
|
|
97
|
+
* the gateway still gets a well-formed, per-call-unique traceparent either
|
|
98
|
+
* way. Never throws; `isSpanContextValid` is the same guard
|
|
99
|
+
* `otel_propagation.ts`'s own propagators use.
|
|
100
|
+
*/
|
|
101
|
+
export declare function freshTraceparent(): string;
|
|
45
102
|
/** Exported so `doctor.ts`'s reachability probe agrees with what a real dispatch resolves to — see its call site for why a `??`/`||` mismatch here matters. */
|
|
46
103
|
export declare function ollamaBaseUrl(): string;
|
|
47
104
|
/**
|
|
48
105
|
* Registers `modelId` (the part after `ollama/` in an agent's `model`
|
|
49
106
|
* config) with Flue's provider registry, alongside every other `ollama/*`
|
|
50
|
-
* id ever registered this process.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* `
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
107
|
+
* id ever registered this process. NOT idempotent w.r.t. `ctx` (see MAJOR-D
|
|
108
|
+
* below) — every call re-runs the union re-registration (dynamic imports
|
|
109
|
+
* are cheap after the first, and `setProvider()` is a cheap in-memory
|
|
110
|
+
* upsert), so this id's `Model.headers` always reflect the MOST RECENT
|
|
111
|
+
* `ctx` this function was called with, not just the first. A concurrent
|
|
112
|
+
* call for the SAME id joins the in-flight registration rather than running
|
|
113
|
+
* a second one in parallel (see `inflight`'s doc); `registeredIds` itself
|
|
114
|
+
* is only ever updated AFTER `setProvider()` succeeds, so a failed attempt
|
|
115
|
+
* (a bad install, a bundler that can't resolve the deep `.lazy` subpath, a
|
|
116
|
+
* future validation error) leaves the id unregistered and eligible for a
|
|
117
|
+
* real retry — not permanently and misleadingly marked "done" while
|
|
118
|
+
* nothing is actually registered.
|
|
59
119
|
*
|
|
60
120
|
* Must complete before the FIRST Flue dispatch that names this model
|
|
61
121
|
* (agent_flue.ts's `run()` awaits this before `ensureRuntime()`/`start()`),
|
|
62
|
-
* but is equally safe to call again later with a new id
|
|
63
|
-
*
|
|
64
|
-
* added without orphaning the first (see the `registeredIds` doc
|
|
122
|
+
* but is equally safe to call again later with a new id, or the SAME id
|
|
123
|
+
* again, mid-process — a new id's union re-registration is how a second
|
|
124
|
+
* model gets added without orphaning the first (see the `registeredIds` doc
|
|
125
|
+
* above); a repeat of the SAME id is how MAJOR-D below is fixed.
|
|
126
|
+
*
|
|
127
|
+
* MAJOR-D (fixed): `ctx`, when given AND `isFluePropagationInstalled()` is
|
|
128
|
+
* false (see the "Gateway headers" section above — when it's true, these
|
|
129
|
+
* two headers come from the per-request `GatewayHeadersPropagator`
|
|
130
|
+
* instead), is stamped onto this id's `Model.headers` as
|
|
131
|
+
* `x-correlation-id`/`x-spf-agent`. A PRIOR version of this function
|
|
132
|
+
* returned immediately for an already-registered id (a false comment
|
|
133
|
+
* claimed "one spf process runs one adw_id for its whole lifetime" to
|
|
134
|
+
* justify this) — which meant every later agent/adw_id sharing a model id
|
|
135
|
+
* within one process (spf `loop`/`fanout`/`watch`, which run many adw_ids
|
|
136
|
+
* in ONE process, `fanout` concurrently) silently kept the FIRST
|
|
137
|
+
* registration's headers forever. Re-running the full registration on every
|
|
138
|
+
* call, unconditionally, fixes that for every case except one, which
|
|
139
|
+
* remains and is not silently swallowed: two flue agents dispatching
|
|
140
|
+
* CONCURRENTLY (not sequentially) to the SAME `ollama/<id>` model id race on
|
|
141
|
+
* `registrationContext`/`setProvider()` — whichever registration's
|
|
142
|
+
* `setProvider()` call lands last wins the headers BOTH calls' subsequent
|
|
143
|
+
* dispatches see, until the next registration for that id. This is a
|
|
144
|
+
* `fanout` concurrency > 1 scenario specifically (two DIFFERENT agents,
|
|
145
|
+
* same process, same model id, truly overlapping registrations) — a
|
|
146
|
+
* sequential loop/watch never hits it, since each call's `await` completes
|
|
147
|
+
* before the next one starts.
|
|
65
148
|
*/
|
|
66
|
-
export declare function registerOllamaModel(modelId: string): Promise<void>;
|
|
149
|
+
export declare function registerOllamaModel(modelId: string, ctx?: GatewayCallContext): Promise<void>;
|
|
67
150
|
/** Test-only: the most recently constructed provider object (see `lastProvider`'s doc). */
|
|
68
151
|
export declare function providerForTest(): Provider<"openai-completions"> | undefined;
|
|
69
152
|
/** Test-only: forgets accumulated ids so test files don't leak into each other. Does not touch Flue's own registry — pair with `resetModelsForTests()` from `@flue/runtime/internal`. */
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
* `type` imports below are erased at compile time (verbatimModuleSyntax) and
|
|
42
42
|
* cost nothing at runtime either way.
|
|
43
43
|
*/
|
|
44
|
+
import { context as contextApi, isSpanContextValid, trace as traceApi } from "@opentelemetry/api";
|
|
45
|
+
import { isFluePropagationInstalled, X_CORRELATION_ID_HEADER, X_SPF_AGENT_HEADER } from "./otel_propagation.js";
|
|
46
|
+
import { newId } from "./utils.js";
|
|
44
47
|
// Ollama has no auth of its own — `pi-ai`'s auth resolution always calls
|
|
45
48
|
// `getClientApiKey()` before a dispatch, and that call throws "No API key
|
|
46
49
|
// for provider: ollama" if the resolved key is falsy (verified live: the
|
|
@@ -51,6 +54,65 @@
|
|
|
51
54
|
// sends anywhere Ollama would look at it: Ollama's OpenAI-compatible server
|
|
52
55
|
// does not check the Authorization header's contents.
|
|
53
56
|
const DUMMY_API_KEY = "ollama-local-unused";
|
|
57
|
+
/**
|
|
58
|
+
* `OLLAMA_API_KEY`, trimmed, when the operator has set one — e.g. the
|
|
59
|
+
* Briefs gateway (Envoy AI Gateway) enforces a per-client bearer and 401s
|
|
60
|
+
* the dummy key `DUMMY_API_KEY` was designed for a bare local server that
|
|
61
|
+
* checks nothing. Falls back to the dummy exactly as before when unset, so
|
|
62
|
+
* THIS function's own return value — the resolved `Authorization` bearer —
|
|
63
|
+
* is byte-identical to pre-gateway behavior for a bare local Ollama server
|
|
64
|
+
* (the common case this module was built for). MINOR-H: that is narrower
|
|
65
|
+
* than "the whole request is unchanged" — it is not, even with
|
|
66
|
+
* `OLLAMA_API_KEY` unset: a `traceparent` (and, once `agent_flue.ts` has an
|
|
67
|
+
* adw_id/agent_name to give it, `x-correlation-id`/`x-spf-agent`) is ALWAYS
|
|
68
|
+
* sent now, gateway or no gateway (see the "Gateway headers" section below).
|
|
69
|
+
* A bare local Ollama server ignores headers it doesn't recognize, so this
|
|
70
|
+
* is harmless — just not byte-identical. Read fresh inside `resolve()` (see
|
|
71
|
+
* its call site below) — never cached — so a key exported mid-process (or
|
|
72
|
+
* changed) takes effect on the very next dispatch with no re-registration.
|
|
73
|
+
*
|
|
74
|
+
* MINOR 3: exported so `doctor.ts`'s `OLLAMA_BASE_URL reachability` probe
|
|
75
|
+
* calls this SAME function rather than reading `process.env.OLLAMA_API_KEY`
|
|
76
|
+
* raw — a prior version of that probe sent NO `Authorization` header at all
|
|
77
|
+
* when the env var was unset, which diverges from what a real dispatch
|
|
78
|
+
* sends (the dummy bearer below, always). Against a gateway that rejects a
|
|
79
|
+
* request with no `Authorization` header at all differently than one with a
|
|
80
|
+
* wrong/dummy bearer, that divergence could make doctor report reachable
|
|
81
|
+
* when a real dispatch would 401, or vice versa. Calling `ollamaApiKey()` in
|
|
82
|
+
* both places means doctor's probe and a real dispatch send byte-identical
|
|
83
|
+
* bearers for the same env state.
|
|
84
|
+
*/
|
|
85
|
+
export function ollamaApiKey() {
|
|
86
|
+
const key = (process.env.OLLAMA_API_KEY ?? "").trim();
|
|
87
|
+
return key || DUMMY_API_KEY;
|
|
88
|
+
}
|
|
89
|
+
/** Must NEVER be sent — Envoy/Switchyard own it end-to-end; a client-supplied value breaks their sampling. Exported only so tests can assert its absence by name, not a literal string. The sole surviving export of this name in the codebase — see MINOR-G in this change's review; `otel_propagation.ts` no longer has one now that `XRequestIdPropagator` is gone (BLOCKER B). */
|
|
90
|
+
export const X_REQUEST_ID_HEADER = "x-request-id";
|
|
91
|
+
/**
|
|
92
|
+
* A fresh W3C `traceparent` for one outbound call — used only on the NOT
|
|
93
|
+
* INSTALLED path (see the section above); when propagation IS installed,
|
|
94
|
+
* `resolve()` does not call this at all, relying entirely on the
|
|
95
|
+
* instrumentation's own per-request injection instead (this is what fixed
|
|
96
|
+
* BLOCKER A/MAJOR-C: this function used to be called unconditionally, and
|
|
97
|
+
* on the installed path it silently reused the one still-open span's
|
|
98
|
+
* traceparent for every call inside that span, which is both a duplicate
|
|
99
|
+
* header AND not actually fresh per call).
|
|
100
|
+
*
|
|
101
|
+
* Reuses the active OTel span context when one is installed and current —
|
|
102
|
+
* the same trace this call's other telemetry already belongs to — falling
|
|
103
|
+
* back to a brand-new random trace/span id pair when there is none (no span
|
|
104
|
+
* active at this exact point, e.g. a stray call before any span opened), so
|
|
105
|
+
* the gateway still gets a well-formed, per-call-unique traceparent either
|
|
106
|
+
* way. Never throws; `isSpanContextValid` is the same guard
|
|
107
|
+
* `otel_propagation.ts`'s own propagators use.
|
|
108
|
+
*/
|
|
109
|
+
export function freshTraceparent() {
|
|
110
|
+
const active = traceApi.getSpanContext(contextApi.active());
|
|
111
|
+
if (active && isSpanContextValid(active)) {
|
|
112
|
+
return `00-${active.traceId}-${active.spanId}-01`;
|
|
113
|
+
}
|
|
114
|
+
return `00-${newId(32)}-${newId(16)}-01`;
|
|
115
|
+
}
|
|
54
116
|
// Advisory only: pi-ai's `openai-completions` api reads this per REQUEST via
|
|
55
117
|
// its own `options.maxTokens`, not from `Model.maxTokens` directly — the
|
|
56
118
|
// field here only feeds Flue's compaction-reserve sizing (moot in practice
|
|
@@ -71,14 +133,25 @@ const DEFAULT_MAX_TOKENS = 8192;
|
|
|
71
133
|
*
|
|
72
134
|
* `OLLAMA_BASE_URL` is read fresh (via `ollamaBaseUrl()`) at each
|
|
73
135
|
* registration call, and the whole union is re-registered at whatever URL
|
|
74
|
-
* is current AT THAT MOMENT
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* per-agent override, which isn't.
|
|
136
|
+
* is current AT THAT MOMENT. `registerOllamaModel` re-runs this rebuild on
|
|
137
|
+
* EVERY call now (see its own doc for why: MAJOR-D's gateway-header
|
|
138
|
+
* re-stamping needs it), so in practice a mid-process `OLLAMA_BASE_URL`
|
|
139
|
+
* change is picked up by the very next dispatch to ANY already-registered
|
|
140
|
+
* id, not just the next brand-new one.
|
|
80
141
|
*/
|
|
81
142
|
const registeredIds = new Set();
|
|
143
|
+
/**
|
|
144
|
+
* The `GatewayCallContext` each model id was MOST RECENTLY registered with
|
|
145
|
+
* — see the "Gateway headers" section above for why `x-correlation-id`/
|
|
146
|
+
* `x-spf-agent` are static per-model (when otel propagation isn't
|
|
147
|
+
* installed) rather than resolved per-call. Keyed by model id so a union
|
|
148
|
+
* re-registration (triggered by ANY registration call, new id or repeat —
|
|
149
|
+
* see `registerOllamaModel`'s MAJOR-D doc) can rebuild every
|
|
150
|
+
* already-registered id's `Model.headers` from the context it MOST
|
|
151
|
+
* RECENTLY got, rather than dropping it or freezing it at first
|
|
152
|
+
* registration.
|
|
153
|
+
*/
|
|
154
|
+
const registrationContext = new Map();
|
|
82
155
|
// Registrations currently in flight, keyed by model id — lets a second
|
|
83
156
|
// caller for the SAME id that arrives before the first `await` resolves
|
|
84
157
|
// join that in-progress registration instead of returning immediately with
|
|
@@ -96,7 +169,25 @@ export function ollamaBaseUrl() {
|
|
|
96
169
|
const raw = (process.env.OLLAMA_BASE_URL ?? "").trim();
|
|
97
170
|
return raw || "http://localhost:11434/v1";
|
|
98
171
|
}
|
|
99
|
-
function modelFor(id, baseUrl) {
|
|
172
|
+
function modelFor(id, baseUrl, ctx) {
|
|
173
|
+
// Static per-model headers — see the "Gateway headers" section above for
|
|
174
|
+
// why `x-correlation-id`/`x-spf-agent` live here rather than in
|
|
175
|
+
// `resolve()`, and ONLY on the NOT INSTALLED path: when
|
|
176
|
+
// `isFluePropagationInstalled()` is true, `GatewayHeadersPropagator`
|
|
177
|
+
// already injects both per real request, correctly attributed per
|
|
178
|
+
// session — stamping them here too would double them up on the wire
|
|
179
|
+
// (BLOCKER A's bug, for these two headers instead of `traceparent`).
|
|
180
|
+
// Omitted entirely (no `headers` key at all) when there is nothing to
|
|
181
|
+
// stamp — propagation installed, or `ctx` absent/empty — so a caller that
|
|
182
|
+
// never passes one, or a run with otel configured, gets a `Model` with no
|
|
183
|
+
// static headers.
|
|
184
|
+
const staticHeaders = {};
|
|
185
|
+
if (!isFluePropagationInstalled()) {
|
|
186
|
+
if (ctx?.adwId)
|
|
187
|
+
staticHeaders[X_CORRELATION_ID_HEADER] = ctx.adwId;
|
|
188
|
+
if (ctx?.agentName)
|
|
189
|
+
staticHeaders[X_SPF_AGENT_HEADER] = ctx.agentName;
|
|
190
|
+
}
|
|
100
191
|
return {
|
|
101
192
|
id,
|
|
102
193
|
name: id,
|
|
@@ -113,30 +204,55 @@ function modelFor(id, baseUrl) {
|
|
|
113
204
|
// silently truncating requests.
|
|
114
205
|
contextWindow: 0,
|
|
115
206
|
maxTokens: DEFAULT_MAX_TOKENS,
|
|
207
|
+
...(Object.keys(staticHeaders).length > 0 ? { headers: staticHeaders } : {}),
|
|
116
208
|
};
|
|
117
209
|
}
|
|
118
210
|
/**
|
|
119
211
|
* Registers `modelId` (the part after `ollama/` in an agent's `model`
|
|
120
212
|
* config) with Flue's provider registry, alongside every other `ollama/*`
|
|
121
|
-
* id ever registered this process.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* `
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
213
|
+
* id ever registered this process. NOT idempotent w.r.t. `ctx` (see MAJOR-D
|
|
214
|
+
* below) — every call re-runs the union re-registration (dynamic imports
|
|
215
|
+
* are cheap after the first, and `setProvider()` is a cheap in-memory
|
|
216
|
+
* upsert), so this id's `Model.headers` always reflect the MOST RECENT
|
|
217
|
+
* `ctx` this function was called with, not just the first. A concurrent
|
|
218
|
+
* call for the SAME id joins the in-flight registration rather than running
|
|
219
|
+
* a second one in parallel (see `inflight`'s doc); `registeredIds` itself
|
|
220
|
+
* is only ever updated AFTER `setProvider()` succeeds, so a failed attempt
|
|
221
|
+
* (a bad install, a bundler that can't resolve the deep `.lazy` subpath, a
|
|
222
|
+
* future validation error) leaves the id unregistered and eligible for a
|
|
223
|
+
* real retry — not permanently and misleadingly marked "done" while
|
|
224
|
+
* nothing is actually registered.
|
|
130
225
|
*
|
|
131
226
|
* Must complete before the FIRST Flue dispatch that names this model
|
|
132
227
|
* (agent_flue.ts's `run()` awaits this before `ensureRuntime()`/`start()`),
|
|
133
|
-
* but is equally safe to call again later with a new id
|
|
134
|
-
*
|
|
135
|
-
* added without orphaning the first (see the `registeredIds` doc
|
|
228
|
+
* but is equally safe to call again later with a new id, or the SAME id
|
|
229
|
+
* again, mid-process — a new id's union re-registration is how a second
|
|
230
|
+
* model gets added without orphaning the first (see the `registeredIds` doc
|
|
231
|
+
* above); a repeat of the SAME id is how MAJOR-D below is fixed.
|
|
232
|
+
*
|
|
233
|
+
* MAJOR-D (fixed): `ctx`, when given AND `isFluePropagationInstalled()` is
|
|
234
|
+
* false (see the "Gateway headers" section above — when it's true, these
|
|
235
|
+
* two headers come from the per-request `GatewayHeadersPropagator`
|
|
236
|
+
* instead), is stamped onto this id's `Model.headers` as
|
|
237
|
+
* `x-correlation-id`/`x-spf-agent`. A PRIOR version of this function
|
|
238
|
+
* returned immediately for an already-registered id (a false comment
|
|
239
|
+
* claimed "one spf process runs one adw_id for its whole lifetime" to
|
|
240
|
+
* justify this) — which meant every later agent/adw_id sharing a model id
|
|
241
|
+
* within one process (spf `loop`/`fanout`/`watch`, which run many adw_ids
|
|
242
|
+
* in ONE process, `fanout` concurrently) silently kept the FIRST
|
|
243
|
+
* registration's headers forever. Re-running the full registration on every
|
|
244
|
+
* call, unconditionally, fixes that for every case except one, which
|
|
245
|
+
* remains and is not silently swallowed: two flue agents dispatching
|
|
246
|
+
* CONCURRENTLY (not sequentially) to the SAME `ollama/<id>` model id race on
|
|
247
|
+
* `registrationContext`/`setProvider()` — whichever registration's
|
|
248
|
+
* `setProvider()` call lands last wins the headers BOTH calls' subsequent
|
|
249
|
+
* dispatches see, until the next registration for that id. This is a
|
|
250
|
+
* `fanout` concurrency > 1 scenario specifically (two DIFFERENT agents,
|
|
251
|
+
* same process, same model id, truly overlapping registrations) — a
|
|
252
|
+
* sequential loop/watch never hits it, since each call's `await` completes
|
|
253
|
+
* before the next one starts.
|
|
136
254
|
*/
|
|
137
|
-
export async function registerOllamaModel(modelId) {
|
|
138
|
-
if (registeredIds.has(modelId))
|
|
139
|
-
return;
|
|
255
|
+
export async function registerOllamaModel(modelId, ctx) {
|
|
140
256
|
const existing = inflight.get(modelId);
|
|
141
257
|
if (existing)
|
|
142
258
|
return existing;
|
|
@@ -156,8 +272,9 @@ export async function registerOllamaModel(modelId) {
|
|
|
156
272
|
// AFTER `setProvider()` below succeeds (see this function's doc).
|
|
157
273
|
const ids = new Set(registeredIds);
|
|
158
274
|
ids.add(modelId);
|
|
275
|
+
registrationContext.set(modelId, ctx ?? {});
|
|
159
276
|
const baseUrl = ollamaBaseUrl();
|
|
160
|
-
const models = [...ids].map((id) => modelFor(id, baseUrl));
|
|
277
|
+
const models = [...ids].map((id) => modelFor(id, baseUrl, registrationContext.get(id)));
|
|
161
278
|
const options = {
|
|
162
279
|
id: "ollama",
|
|
163
280
|
name: "Ollama (local)",
|
|
@@ -165,9 +282,37 @@ export async function registerOllamaModel(modelId) {
|
|
|
165
282
|
auth: {
|
|
166
283
|
apiKey: {
|
|
167
284
|
name: "Ollama (keyless)",
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
285
|
+
// Fresh per real dispatch (pi-ai reinvokes `resolve()` on every
|
|
286
|
+
// `Models.stream()`/`applyAuth()` call, never caching it — see the
|
|
287
|
+
// "Gateway headers" section above) — `apiKey` honors a real
|
|
288
|
+
// `OLLAMA_API_KEY` when the operator set one (e.g. the Briefs
|
|
289
|
+
// gateway's per-client bearer), falling back to the DUMMY_API_KEY
|
|
290
|
+
// a bare keyless local server needs (see its own doc for why that
|
|
291
|
+
// can't just be "no key needed" instead).
|
|
292
|
+
//
|
|
293
|
+
// `headers.traceparent` is minted here ONLY when
|
|
294
|
+
// `isFluePropagationInstalled()` is false — checked fresh on every
|
|
295
|
+
// call, since propagation can be installed partway through this
|
|
296
|
+
// process's lifetime (the first ollama dispatch in a run with
|
|
297
|
+
// otel configured registers the model BEFORE
|
|
298
|
+
// `installFluePropagation()` runs — see `agent_flue.ts`'s `run()`
|
|
299
|
+
// — so a later dispatch on the SAME already-registered model must
|
|
300
|
+
// still re-check, not trust a value baked in at registration
|
|
301
|
+
// time). When installed, `@opentelemetry/instrumentation-undici`
|
|
302
|
+
// already injects a real, fresh `traceparent` for this exact
|
|
303
|
+
// outbound request (see `otel_propagation.ts`); minting a second
|
|
304
|
+
// one here would put TWO `traceparent` headers on the wire
|
|
305
|
+
// (`UndiciInstrumentation` appends, it does not replace) — this
|
|
306
|
+
// was BLOCKER A. `x-correlation-id`/`x-spf-agent` are NEVER set
|
|
307
|
+
// here either way — see `Model.headers` above (not installed) and
|
|
308
|
+
// `GatewayHeadersPropagator` (installed) for where those two
|
|
309
|
+
// actually come from.
|
|
310
|
+
resolve: async () => ({
|
|
311
|
+
auth: {
|
|
312
|
+
apiKey: ollamaApiKey(),
|
|
313
|
+
...(isFluePropagationInstalled() ? {} : { headers: { traceparent: freshTraceparent() } }),
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
171
316
|
},
|
|
172
317
|
},
|
|
173
318
|
models,
|
|
@@ -205,4 +350,5 @@ export function resetOllamaRegistrationForTest() {
|
|
|
205
350
|
registeredIds.clear();
|
|
206
351
|
inflight.clear();
|
|
207
352
|
lastProvider = undefined;
|
|
353
|
+
registrationContext.clear();
|
|
208
354
|
}
|
package/dist/core/otel.js
CHANGED
|
@@ -407,7 +407,15 @@ export function redact(message, secrets) {
|
|
|
407
407
|
function numOrNull(value) {
|
|
408
408
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
409
409
|
}
|
|
410
|
-
/**
|
|
410
|
+
/**
|
|
411
|
+
* UsageBreakdown's token fields -> attribute suffixes. Numbers only, by
|
|
412
|
+
* construction. `billable_tokens` rides alongside `total_tokens` — the SAME
|
|
413
|
+
* split `run_dashboard.tsx`'s live spend line and `estimate.ts`'s cutoff
|
|
414
|
+
* projection now both carry (see `UsageBreakdown.billable_tokens`'s doc
|
|
415
|
+
* comment in `data_types.ts`) — so a Langfuse/OTEL consumer graphing spend
|
|
416
|
+
* against `defaults.max_run_tokens` has the metric the real ceiling check
|
|
417
|
+
* actually uses, not just the display total (cache reads included).
|
|
418
|
+
*/
|
|
411
419
|
const TOKEN_FIELDS = [
|
|
412
420
|
["input_tokens", "spf.tokens.input"],
|
|
413
421
|
["output_tokens", "spf.tokens.output"],
|
|
@@ -415,6 +423,7 @@ const TOKEN_FIELDS = [
|
|
|
415
423
|
["cache_write_tokens", "spf.tokens.cache_write"],
|
|
416
424
|
["reasoning_tokens", "spf.tokens.reasoning"],
|
|
417
425
|
["total_tokens", "spf.tokens.total"],
|
|
426
|
+
["billable_tokens", "spf.tokens.billable"],
|
|
418
427
|
];
|
|
419
428
|
const COST_FIELDS = [
|
|
420
429
|
["input_cost", "spf.cost.input"],
|