@elliotsystems/eai 0.1.7 → 0.1.14
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/bin/eai +55 -0
- package/lib/auth.js +173 -0
- package/lib/context.js +52 -0
- package/lib/login.js +114 -0
- package/lib/setup.js +152 -23
- package/lib/stage.js +92 -11
- package/lib/status.js +2 -0
- package/lib/teams.js +233 -0
- package/package.json +2 -2
- package/plugin/opencode-plugin/src/blocked-turns.ts +145 -0
- package/plugin/opencode-plugin/src/compaction.ts +600 -0
- package/plugin/opencode-plugin/src/elliot-auth.ts +10 -0
- package/plugin/opencode-plugin/src/guardrail-sync.ts +370 -0
- package/plugin/opencode-plugin/src/index.ts +476 -0
- package/plugin/opencode-plugin/src/session-affinity.ts +73 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the Jev guardrail's per-request activation current with the team's
|
|
3
|
+
* REAL, live `Team.jev_policy_enabled` — instead of the one-shot snapshot
|
|
4
|
+
* `eai setup` bakes into `opencode.json`'s static `options.metadata.
|
|
5
|
+
* guardrails` (see elliot-launcher/lib/setup.js's own `elliotProvider`).
|
|
6
|
+
*
|
|
7
|
+
* ## The gap this closes (CLI session-recovery)
|
|
8
|
+
*
|
|
9
|
+
* Web Chat's equivalent bug (terminal/src/opencode/useOpencodeChat.ts) was:
|
|
10
|
+
* an admin's `jev_policy_enabled` change doesn't reach an already-running
|
|
11
|
+
* OpenCode instance until that instance is torn down and rebuilt — fixed on
|
|
12
|
+
* the Web Chat side by (a) checking team-level drift on every request
|
|
13
|
+
* (backend `_reconcile`) and (b) transparently starting a fresh session when
|
|
14
|
+
* the old one's instance is gone.
|
|
15
|
+
*
|
|
16
|
+
* NEITHER half of that fix has a real analogue for the EAI CLI, and this
|
|
17
|
+
* module exists because of that, not in spite of it:
|
|
18
|
+
*
|
|
19
|
+
* - There is no "OpenCode server instance" to replace. Web Chat's
|
|
20
|
+
* OpenCode runtimes are backend-hosted containers/subprocesses, keyed by
|
|
21
|
+
* team, with their own lifecycle (services/opencode/manager.py). The
|
|
22
|
+
* CLI's OpenCode process is the user's own local machine running the
|
|
23
|
+
* real upstream `opencode` binary (elliot-launcher/bin/eai just
|
|
24
|
+
* `spawn`s it with `stdio: "inherit"`) — nothing on Elliot's backend
|
|
25
|
+
* ever tears it down or replaces it. A "replaced instance" literally
|
|
26
|
+
* cannot happen to it.
|
|
27
|
+
* - `elliot-launcher`'s `eai` has zero visibility into the TUI's traffic
|
|
28
|
+
* once spawned (inherited stdio, a real full-screen terminal app) and
|
|
29
|
+
* the OpenCode plugin API (opencode/packages/plugin/src/index.ts) has NO
|
|
30
|
+
* hook that observes a chat response or error — `tool.execute.after`
|
|
31
|
+
* only covers TOOL calls, not the chat completion itself. So "detect
|
|
32
|
+
* this message got blocked, then resend it" is not implementable from
|
|
33
|
+
* either layer available to this integration: there is no hook to
|
|
34
|
+
* detect it from.
|
|
35
|
+
*
|
|
36
|
+
* What IS real, and what this fixes: the CLI's guardrail activation is a
|
|
37
|
+
* STATIC snapshot, taken once at `eai setup` and never touched again for the
|
|
38
|
+
* life of that file. An admin flipping the toggle after that leaves the CLI
|
|
39
|
+
* silently wrong in BOTH directions until the user re-runs `eai setup`:
|
|
40
|
+
* - turned ON after setup -> the CLI never guards anything (a real
|
|
41
|
+
* enforcement gap, not just a UX one);
|
|
42
|
+
* - turned OFF after setup -> the CLI keeps blocking every single prompt
|
|
43
|
+
* forever (the exact behavior reported: "for every prompts... fix it").
|
|
44
|
+
*
|
|
45
|
+
* `chat.params` (opencode/packages/plugin/src/index.ts) is the one hook that
|
|
46
|
+
* runs on EVERY outgoing request, before it is sent, with write access to
|
|
47
|
+
* the exact `options` object that becomes the request body (traced through
|
|
48
|
+
* session/llm/request.ts: `options = mergeOptions(base, model.options, ...)`
|
|
49
|
+
* is passed into `chat.params`'s `output.options`, and `model.options` is
|
|
50
|
+
* precisely where `eai setup` wrote `metadata.guardrails`). Using it here
|
|
51
|
+
* means the correct, CURRENT guardrail state is used on the message's one
|
|
52
|
+
* and only attempt — there is no failure to recover from, no retry, and
|
|
53
|
+
* therefore none of the duplicate-request/duplicate-tool-execution risk a
|
|
54
|
+
* retry-based design would carry.
|
|
55
|
+
*
|
|
56
|
+
* ## Fail-safe direction
|
|
57
|
+
*
|
|
58
|
+
* A live check that cannot complete (offline, backend down, expired token)
|
|
59
|
+
* returns `null` and changes NOTHING — the request goes out with whatever
|
|
60
|
+
* the static config already had, exactly as it did before this module
|
|
61
|
+
* existed. This can under-protect (an admin's brand-new enable is missed
|
|
62
|
+
* while offline) but never over-blocks and never throws, which is the
|
|
63
|
+
* right trade for a CLI tool that must keep working offline: this was never
|
|
64
|
+
* the enforcement boundary (LiteLLM's own guardrail is, and it fails CLOSED
|
|
65
|
+
* on its own uncertainty — see jev_decisions_logic.py's CONFIDENCE_THRESHOLD
|
|
66
|
+
* — because THAT failure is security-load-bearing). This module only
|
|
67
|
+
* decides which flag to send; a transport failure here is an availability
|
|
68
|
+
* question, not a security one.
|
|
69
|
+
*/
|
|
70
|
+
import { readElliotConfig, type ElliotConfig } from "./elliot-auth.js"
|
|
71
|
+
|
|
72
|
+
export const JEV_GUARDRAIL_NAME = "jev-policy"
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Jev audit (2026-10, G2): the POST-CALL counterpart to `JEV_GUARDRAIL_NAME`
|
|
76
|
+
* — must match `Team.jev_output_validation_enabled`'s own LiteLLM guardrail
|
|
77
|
+
* name (`deploy/litellm/config.yaml`'s `"jev-output-validation"` entry,
|
|
78
|
+
* `backend/app/services/jev_guardrail_names.py`'s `POST_CALL_GUARDRAIL_NAME`).
|
|
79
|
+
* Before this constant/the functions below existed, this file only ever
|
|
80
|
+
* live-synced `jev-policy` — an admin enabling or disabling
|
|
81
|
+
* `jev_output_validation_enabled` had ZERO effect on an already-`eai setup`
|
|
82
|
+
* CLI session until the user re-ran setup, the exact bug this whole module
|
|
83
|
+
* exists to prevent for the pre-call flag (see this file's own top docstring)
|
|
84
|
+
* but silently left unfixed for the post-call one.
|
|
85
|
+
*/
|
|
86
|
+
export const OUTPUT_VALIDATION_GUARDRAIL_NAME = "jev-output-validation"
|
|
87
|
+
|
|
88
|
+
/** How long a live answer is trusted before the next request re-checks.
|
|
89
|
+
* Short enough that an admin's change reaches the CLI within one coffee
|
|
90
|
+
* break of it taking effect; long enough that a user typing several
|
|
91
|
+
* messages in a row does not turn every one into a network round trip. */
|
|
92
|
+
const CACHE_TTL_MS = 15_000
|
|
93
|
+
|
|
94
|
+
/** Per-request timeout for the live check itself. A hung connection must
|
|
95
|
+
* not add a user-visible stall to every single message. */
|
|
96
|
+
const CHECK_TIMEOUT_MS = 3_000
|
|
97
|
+
|
|
98
|
+
interface CacheEntry {
|
|
99
|
+
jevPolicyEnabled: boolean
|
|
100
|
+
/** Jev audit (2026-10, G2). */
|
|
101
|
+
jevOutputValidationEnabled: boolean
|
|
102
|
+
/** Fast Jev Compaction pilot (spec: `specs/2026-10-29-fast-jev-
|
|
103
|
+
* compaction-pilot.md`, task T7) -- `Team.eai_compaction_enabled`, read
|
|
104
|
+
* from this SAME `GET /teams/{id}` response, never a second round trip. */
|
|
105
|
+
compactionEnabled: boolean
|
|
106
|
+
teamId: string
|
|
107
|
+
expiresAt: number
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let cache: CacheEntry | null = null
|
|
111
|
+
|
|
112
|
+
/** Test-only: forget whatever the last live check learned, so a test does
|
|
113
|
+
* not inherit state another test left behind (module-level cache is
|
|
114
|
+
* otherwise invisible from outside this file). */
|
|
115
|
+
export function resetGuardrailSyncCache(): void {
|
|
116
|
+
cache = null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface LiveCheckDeps {
|
|
120
|
+
/** Overridable for tests — never the real network in a unit test. */
|
|
121
|
+
fetchImpl?: typeof fetch
|
|
122
|
+
now?: () => number
|
|
123
|
+
/** Overridable so a test does not have to write a real file to disk for
|
|
124
|
+
* every case; defaults to the real `~/.elliot/config.json` reader. */
|
|
125
|
+
readConfig?: () => ElliotConfig | null
|
|
126
|
+
/**
|
|
127
|
+
* The team THIS OUTGOING REQUEST is actually scoped to — the ground
|
|
128
|
+
* truth already baked into `options.headers["x-litellm-spend-logs-
|
|
129
|
+
* metadata"]` by `teamconfig.py`'s own `_litellm_identity_headers` at
|
|
130
|
+
* provisioning time (see `extractInstanceTeamId` below, which the
|
|
131
|
+
* `chat.params` hook uses to derive this). NOT read from `~/.elliot/
|
|
132
|
+
* config.json` — that file is per-MACHINE, written once by `eai setup`
|
|
133
|
+
* for one specific person/team/backend.
|
|
134
|
+
*
|
|
135
|
+
* Required to trust the live check at all: a backend-hosted Web Chat
|
|
136
|
+
* OpenCode runtime, in this project's local-dev mode (`OPENCODE_
|
|
137
|
+
* RUNTIME_SUBPROCESS` — same user, same filesystem, no isolation), can
|
|
138
|
+
* end up running under a real developer's OWN `eai setup` login for a
|
|
139
|
+
* completely different team, tenant, or even backend (e.g. staging)
|
|
140
|
+
* than the one THIS request belongs to. Live-confirmed this session: a
|
|
141
|
+
* local "Test" team's guardrail toggle was silently overridden by a
|
|
142
|
+
* staging account's config for an unrelated team, because nothing here
|
|
143
|
+
* had ever checked that the two team ids agreed before trusting the
|
|
144
|
+
* live answer.
|
|
145
|
+
*
|
|
146
|
+
* `undefined` (the default) skips this check entirely — an existing
|
|
147
|
+
* caller with no way to know its own instance team id (there isn't one
|
|
148
|
+
* for the real EAI CLI, which never shares a filesystem with anyone
|
|
149
|
+
* else's session) keeps working exactly as before. Only an EXPLICIT,
|
|
150
|
+
* KNOWN mismatch (both ids present and different) suppresses the live
|
|
151
|
+
* check; this narrows an unsafe case, it does not add a new
|
|
152
|
+
* requirement for callers that already work correctly.
|
|
153
|
+
*/
|
|
154
|
+
instanceTeamId?: string | null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Both guardrail toggles' current, live values from the backend — fetched
|
|
159
|
+
* and cached TOGETHER from the one `GET /teams/{id}` call that already
|
|
160
|
+
* returns both `jev_policy_enabled` and `jev_output_validation_enabled`
|
|
161
|
+
* (Jev audit, 2026-10, G2: previously only the pre-call flag was ever
|
|
162
|
+
* fetched/cached here, so the post-call one had no live-sync mechanism at
|
|
163
|
+
* all). Returns `null` under the exact same conditions `liveJevPolicyEnabled`
|
|
164
|
+
* always has (see its own docstring, preserved below) — no team context, an
|
|
165
|
+
* instance/request team-id mismatch, or a failed check with no usable cache.
|
|
166
|
+
*
|
|
167
|
+
* Fast Jev Compaction pilot (T7): also reads `eai_compaction_enabled` from
|
|
168
|
+
* this same response/cache entry -- one more field off a call this module
|
|
169
|
+
* already makes, not a second network round trip.
|
|
170
|
+
*/
|
|
171
|
+
async function liveJevGuardrailFlags(
|
|
172
|
+
deps: LiveCheckDeps = {},
|
|
173
|
+
): Promise<{ jevPolicyEnabled: boolean; jevOutputValidationEnabled: boolean; compactionEnabled: boolean } | null> {
|
|
174
|
+
const now = deps.now ?? Date.now
|
|
175
|
+
const fetchImpl = deps.fetchImpl ?? fetch
|
|
176
|
+
const readConfig = deps.readConfig ?? readElliotConfig
|
|
177
|
+
const config = readConfig()
|
|
178
|
+
|
|
179
|
+
// No team context at all -- a tenant-wide (owner/admin) setup, or a
|
|
180
|
+
// config predating team scoping. Nothing team-level to check live;
|
|
181
|
+
// genuinely unknown rather than "disabled."
|
|
182
|
+
if (!config?.backend_url || !config.tenant_id || !config.team_id || !config.jwt_token) {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The local machine's own login config is for a DIFFERENT team than the
|
|
187
|
+
// one this specific request is scoped to -- trusting it would silently
|
|
188
|
+
// apply (or strip) a guardrail based on someone else's team/tenant/
|
|
189
|
+
// backend. Treat exactly like every other "cannot determine" case above
|
|
190
|
+
// rather than guessing. Only fires when BOTH ids are actually known;
|
|
191
|
+
// `undefined`/`null` here (the default for every existing caller) never
|
|
192
|
+
// triggers it.
|
|
193
|
+
if (
|
|
194
|
+
deps.instanceTeamId !== undefined &&
|
|
195
|
+
deps.instanceTeamId !== null &&
|
|
196
|
+
deps.instanceTeamId !== config.team_id
|
|
197
|
+
) {
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (cache && cache.teamId === config.team_id && now() < cache.expiresAt) {
|
|
202
|
+
return {
|
|
203
|
+
jevPolicyEnabled: cache.jevPolicyEnabled,
|
|
204
|
+
jevOutputValidationEnabled: cache.jevOutputValidationEnabled,
|
|
205
|
+
compactionEnabled: cache.compactionEnabled,
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const controller = new AbortController()
|
|
210
|
+
const timer = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS)
|
|
211
|
+
try {
|
|
212
|
+
const base = config.backend_url.replace(/\/+$/, "")
|
|
213
|
+
const res = await fetchImpl(`${base}/teams/${encodeURIComponent(config.team_id)}`, {
|
|
214
|
+
headers: { Authorization: `Bearer ${config.jwt_token}` },
|
|
215
|
+
signal: controller.signal,
|
|
216
|
+
})
|
|
217
|
+
if (!res.ok) throw new Error(`backend returned ${res.status}`)
|
|
218
|
+
const body: unknown = await res.json()
|
|
219
|
+
const record = body && typeof body === "object" ? (body as Record<string, unknown>) : {}
|
|
220
|
+
const jevPolicyEnabled = Boolean(record.jev_policy_enabled)
|
|
221
|
+
const jevOutputValidationEnabled = Boolean(record.jev_output_validation_enabled)
|
|
222
|
+
const compactionEnabled = Boolean(record.eai_compaction_enabled)
|
|
223
|
+
cache = {
|
|
224
|
+
jevPolicyEnabled,
|
|
225
|
+
jevOutputValidationEnabled,
|
|
226
|
+
compactionEnabled,
|
|
227
|
+
teamId: config.team_id,
|
|
228
|
+
expiresAt: now() + CACHE_TTL_MS,
|
|
229
|
+
}
|
|
230
|
+
return { jevPolicyEnabled, jevOutputValidationEnabled, compactionEnabled }
|
|
231
|
+
} catch {
|
|
232
|
+
// Offline, backend unreachable, expired token, or a malformed response.
|
|
233
|
+
// Never throw into the chat.params hook over this -- a CLI message must
|
|
234
|
+
// still be sendable without a network round trip succeeding. Fall back
|
|
235
|
+
// to a still-fresh cached value for this same team if one exists;
|
|
236
|
+
// otherwise genuinely unknown.
|
|
237
|
+
return cache && cache.teamId === config.team_id && now() < cache.expiresAt
|
|
238
|
+
? {
|
|
239
|
+
jevPolicyEnabled: cache.jevPolicyEnabled,
|
|
240
|
+
jevOutputValidationEnabled: cache.jevOutputValidationEnabled,
|
|
241
|
+
compactionEnabled: cache.compactionEnabled,
|
|
242
|
+
}
|
|
243
|
+
: null
|
|
244
|
+
} finally {
|
|
245
|
+
clearTimeout(timer)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The team's current `jev_policy_enabled`, live from the backend — or
|
|
251
|
+
* `null` when it cannot be determined (no team context, not logged in, the
|
|
252
|
+
* check itself failed, or the local config belongs to a DIFFERENT team than
|
|
253
|
+
* this request — see `instanceTeamId`), which callers must read as "use
|
|
254
|
+
* whatever is already configured," never as "disabled."
|
|
255
|
+
*/
|
|
256
|
+
export async function liveJevPolicyEnabled(deps: LiveCheckDeps = {}): Promise<boolean | null> {
|
|
257
|
+
const flags = await liveJevGuardrailFlags(deps)
|
|
258
|
+
return flags ? flags.jevPolicyEnabled : null
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The POST-CALL counterpart to `liveJevPolicyEnabled` — the team's current
|
|
263
|
+
* `jev_output_validation_enabled`, live from the backend, with the exact
|
|
264
|
+
* same `null`-means-"leave it alone" contract (Jev audit, 2026-10, G2).
|
|
265
|
+
* Shares `liveJevPolicyEnabled`'s own cache/fetch under the hood (one
|
|
266
|
+
* `GET /teams/{id}` call answers both), so calling both on the same request
|
|
267
|
+
* costs at most one network round trip, not two.
|
|
268
|
+
*/
|
|
269
|
+
export async function liveJevOutputValidationEnabled(deps: LiveCheckDeps = {}): Promise<boolean | null> {
|
|
270
|
+
const flags = await liveJevGuardrailFlags(deps)
|
|
271
|
+
return flags ? flags.jevOutputValidationEnabled : null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Fast Jev Compaction pilot (spec: `specs/2026-10-29-fast-jev-compaction-
|
|
276
|
+
* pilot.md`, task T7) — the team's current `Team.eai_compaction_enabled`,
|
|
277
|
+
* live from the backend, with the exact same `null`-means-"leave it alone"
|
|
278
|
+
* contract as `liveJevPolicyEnabled`/`liveJevOutputValidationEnabled`.
|
|
279
|
+
* Shares the same cached `GET /teams/{id}` call under the hood, so calling
|
|
280
|
+
* this alongside either (or both) of the other two costs at most one
|
|
281
|
+
* network round trip, not two or three.
|
|
282
|
+
*
|
|
283
|
+
* Callers (index.ts's `experimental.chat.messages.transform` hook) must
|
|
284
|
+
* treat `null` and `false` identically here: skip compaction and leave
|
|
285
|
+
* `output.messages` exactly as `stripBlockedTurns` left them. Unlike the
|
|
286
|
+
* Jev guardrails, there is no security-load-bearing reason to fail closed
|
|
287
|
+
* on uncertainty -- this flag only decides whether a token-reduction pass
|
|
288
|
+
* runs at all, so "do nothing" is always the safe default when the answer
|
|
289
|
+
* is not confidently known.
|
|
290
|
+
*/
|
|
291
|
+
export async function liveCompactionEnabled(deps: LiveCheckDeps = {}): Promise<boolean | null> {
|
|
292
|
+
const flags = await liveJevGuardrailFlags(deps)
|
|
293
|
+
return flags ? flags.compactionEnabled : null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Applies a live guardrail decision to the outgoing request's `options`,
|
|
298
|
+
* mutating `metadata.guardrails` to match exactly — adding `guardrailName`
|
|
299
|
+
* when live-enabled, removing it (even if the STATIC config baked it in at
|
|
300
|
+
* `eai setup` time) when live-disabled. A `null` decision (see
|
|
301
|
+
* `liveJevPolicyEnabled`'s own docstring) changes nothing, leaving whatever
|
|
302
|
+
* the static config/an earlier plugin already set untouched.
|
|
303
|
+
*
|
|
304
|
+
* `guardrailName` defaults to `JEV_GUARDRAIL_NAME` (pre-call) so every
|
|
305
|
+
* existing 2-argument call site is unaffected; pass
|
|
306
|
+
* `OUTPUT_VALIDATION_GUARDRAIL_NAME` for the post-call decision (Jev audit,
|
|
307
|
+
* 2026-10, G2). Two calls targeting different names compose safely — each
|
|
308
|
+
* only ever adds/removes its OWN name from the shared `guardrails` array,
|
|
309
|
+
* never touching the other's entry, so calling this once for each flag (as
|
|
310
|
+
* `index.ts`'s `chat.params` hook now does) correctly reflects both toggles
|
|
311
|
+
* independently on the same request.
|
|
312
|
+
*
|
|
313
|
+
* Pure and synchronous on purpose: the async live-check already happened by
|
|
314
|
+
* the time this runs, so applying its result cannot itself introduce a
|
|
315
|
+
* second network call, a retry, or any other source of a duplicate request.
|
|
316
|
+
*/
|
|
317
|
+
export function applyGuardrailDecision(
|
|
318
|
+
options: Record<string, unknown>,
|
|
319
|
+
decision: boolean | null,
|
|
320
|
+
guardrailName: string = JEV_GUARDRAIL_NAME,
|
|
321
|
+
): void {
|
|
322
|
+
if (decision === null) return
|
|
323
|
+
|
|
324
|
+
const metadata =
|
|
325
|
+
options.metadata && typeof options.metadata === "object" && !Array.isArray(options.metadata)
|
|
326
|
+
? (options.metadata as Record<string, unknown>)
|
|
327
|
+
: (options.metadata = {} as Record<string, unknown>)
|
|
328
|
+
|
|
329
|
+
const existing = Array.isArray(metadata.guardrails)
|
|
330
|
+
? (metadata.guardrails as unknown[]).filter((g): g is string => typeof g === "string")
|
|
331
|
+
: []
|
|
332
|
+
const withoutThisName = existing.filter((g) => g !== guardrailName)
|
|
333
|
+
|
|
334
|
+
metadata.guardrails = decision ? [...withoutThisName, guardrailName] : withoutThisName
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The team id THIS specific outgoing request is scoped to, read from the
|
|
339
|
+
* SAME static header `teamconfig.py`'s `_litellm_identity_headers` already
|
|
340
|
+
* baked into `options.headers["x-litellm-spend-logs-metadata"]` at
|
|
341
|
+
* provisioning time — e.g. `{"tenant_id":"...","team_id":"...",
|
|
342
|
+
* "source":"opencode"}`. This is ground truth about the request regardless
|
|
343
|
+
* of which real person's OS login happens to be running the process, which
|
|
344
|
+
* is exactly why it is the right thing to compare `~/.elliot/config.json`'s
|
|
345
|
+
* own `team_id` against (see `LiveCheckDeps.instanceTeamId`'s own
|
|
346
|
+
* docstring).
|
|
347
|
+
*
|
|
348
|
+
* Returns `undefined` (not `null`) for "could not determine" — `undefined`
|
|
349
|
+
* is `LiveCheckDeps.instanceTeamId`'s own "skip this check" value, so a
|
|
350
|
+
* request shape this function does not recognize degrades to the EXACT
|
|
351
|
+
* pre-existing behavior (trust whatever `~/.elliot/config.json` says) rather
|
|
352
|
+
* than a new, stricter failure mode nobody asked for.
|
|
353
|
+
*/
|
|
354
|
+
export function extractInstanceTeamId(options: Record<string, unknown> | null | undefined): string | undefined {
|
|
355
|
+
const headers = options?.["headers"]
|
|
356
|
+
if (!headers || typeof headers !== "object") return undefined
|
|
357
|
+
const raw = (headers as Record<string, unknown>)["x-litellm-spend-logs-metadata"]
|
|
358
|
+
if (typeof raw !== "string") return undefined
|
|
359
|
+
try {
|
|
360
|
+
const parsed: unknown = JSON.parse(raw)
|
|
361
|
+
const teamId =
|
|
362
|
+
parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>)["team_id"] : undefined
|
|
363
|
+
return typeof teamId === "string" && teamId ? teamId : undefined
|
|
364
|
+
} catch {
|
|
365
|
+
// Malformed/unexpected header shape -- "could not determine" (see this
|
|
366
|
+
// function's own docstring), never a thrown error over a header this
|
|
367
|
+
// module merely reads and does not own the shape of.
|
|
368
|
+
return undefined
|
|
369
|
+
}
|
|
370
|
+
}
|