@openhoo/hoopilot 2.1.7 → 2.1.9
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 +22 -2
- package/dist/{chunk-2GLKVNAA.js → chunk-FH6WSFOC.js} +29 -1
- package/dist/chunk-FH6WSFOC.js.map +1 -0
- package/dist/cli.js +323 -103
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +330 -102
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-2GLKVNAA.js.map +0 -1
package/README.md
CHANGED
|
@@ -369,7 +369,7 @@ docker run --rm -v hoopilot-data:/data ghcr.io/openhoo/hoopilot login --print-ke
|
|
|
369
369
|
|
|
370
370
|
## Logging
|
|
371
371
|
|
|
372
|
-
Hoopilot uses Pino for structured logs. Server startup, request
|
|
372
|
+
Hoopilot uses Pino for structured logs. Server startup, request errors, upstream Copilot failures, model-list fallback, auth failures, and update-check diagnostics are logged with stable event names and request IDs. Successful request completion logs are disabled by default; pass `--access-log` or set `HOOPILOT_ACCESS_LOG=1` to enable them.
|
|
373
373
|
|
|
374
374
|
Logs never include request bodies, prompt text, completions, stream chunks, OAuth tokens, API keys, authorization headers, cookies, or auth-file contents.
|
|
375
375
|
|
|
@@ -379,6 +379,12 @@ Console logs default to pretty output at `info` level:
|
|
|
379
379
|
hoopilot --log-level info --log-format pretty
|
|
380
380
|
```
|
|
381
381
|
|
|
382
|
+
When access logs are enabled, pretty logs keep common request and diagnostic fields inline for terminal use:
|
|
383
|
+
|
|
384
|
+
```text
|
|
385
|
+
INFO [16:40:14]: request completed component=server event=http.request.completed method=POST path=/v1/chat/completions status=200 duration=42.37ms stream=true requestId=req-test
|
|
386
|
+
```
|
|
387
|
+
|
|
382
388
|
For newline-delimited JSON:
|
|
383
389
|
|
|
384
390
|
```sh
|
|
@@ -395,7 +401,9 @@ Hoopilot tracks token usage, request counts, and latency in memory while the ser
|
|
|
395
401
|
- `GET /v1/usage` returns JSON combining the proxy metrics snapshot with live Copilot quota fetched from GitHub and cached for 60 seconds. If quota cannot be read, `copilot` is `null` and `copilot_error` explains why. The snapshot's `proxy.githubRateLimit` field reports the most recent GitHub REST rate-limit budget per resource (`limit`, `remaining`, `used`, `resetAt`, `retryAfterSeconds`, `observedAt`).
|
|
396
402
|
- `hoopilot usage` prints your Copilot plan and quota — and, when GitHub returns them, your GitHub API rate-limit budget — from the command line.
|
|
397
403
|
|
|
398
|
-
Token usage is read from the upstream `usage` object. For streaming chat completions, usage is only available when the client sends `stream_options: {"include_usage": true}`; Hoopilot does not inject that flag.
|
|
404
|
+
Token usage is read from the upstream `usage` object when Hoopilot is already parsing a compatibility response, or when full token accounting is enabled. For streaming chat completions, usage is only available when the client sends `stream_options: {"include_usage": true}`; Hoopilot does not inject that flag.
|
|
405
|
+
|
|
406
|
+
The default `HOOPILOT_USAGE_ACCOUNTING=basic` mode skips token extraction from pass-through response bodies and SSE streams while still recording usage from compatibility responses Hoopilot already has to parse. Set `HOOPILOT_USAGE_ACCOUNTING=full` when exact pass-through stream/body token accounting matters more than CPU use. Set `HOOPILOT_USAGE_ACCOUNTING=off` to skip token accounting entirely. Request counts, upstream counts, latency, in-flight metrics, `/metrics`, and `/v1/usage` still work in every mode. The `hoopilot_token_extraction_total{outcome="extracted"|"missing"}` counter (mirrored in `/v1/usage` as `proxy.tokens.extraction`) tracks how often a completion reported usage versus not.
|
|
399
407
|
|
|
400
408
|
GitHub API usage is read from the `x-ratelimit-*` response headers that `api.github.com` returns on the `copilot_internal/user` quota call Hoopilot already makes, so it costs no extra request. (The Copilot completion host `api.githubcopilot.com` does not currently emit these headers, so per-completion rate-limit data is not yet available there.)
|
|
401
409
|
|
|
@@ -446,6 +454,7 @@ Server and local-client settings:
|
|
|
446
454
|
| `HOOPILOT_ALLOWED_ORIGINS` | Comma-separated browser origins allowed to make cross-origin requests. Loopback origins are always allowed; every other origin is blocked. |
|
|
447
455
|
| `HOOPILOT_ALLOW_UNAUTHENTICATED` / `--allow-unauthenticated` | Allow non-loopback binds without a local API key. |
|
|
448
456
|
| `HOOPILOT_STREAM_MODE` / `--stream-mode` | `auto`, `live`, or `buffer`. `auto` buffers streams for Windows standalone binaries. `HOOPILOT_STREAMING_PROXY_MODE` is accepted as an alias. |
|
|
457
|
+
| `HOOPILOT_USAGE_ACCOUNTING` / `--usage-accounting` | `basic`, `full`, or `off`. Default: `basic`. `basic` avoids token extraction from pass-through bodies/streams; `full` tracks all reported usage; `off` skips token accounting. |
|
|
449
458
|
|
|
450
459
|
Copilot and GitHub settings:
|
|
451
460
|
|
|
@@ -464,10 +473,21 @@ Logging and update settings:
|
|
|
464
473
|
| --- | --- |
|
|
465
474
|
| `HOOPILOT_LOG_LEVEL` / `--log-level` | `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or `silent`. Default: `info`. |
|
|
466
475
|
| `HOOPILOT_LOG_FORMAT` / `--log-format` | `pretty` or `json`. Default: `pretty`. |
|
|
476
|
+
| `HOOPILOT_ACCESS_LOG` / `--access-log` / `--no-access-log` | Successful request logs are disabled by default. Set `1`/`true` or pass `--access-log` to enable them. Client and server errors are still logged. |
|
|
467
477
|
| `HOOPILOT_UPSTREAM_TIMEOUT_MS` | Time to wait for Copilot response headers before returning `504 copilot_timeout`. Default: `120000`; set `0` to disable. |
|
|
468
478
|
| `HOOPILOT_UPSTREAM_STREAM_IDLE_TIMEOUT_MS` | Time to wait for bytes on a Copilot response body before failing the stream so clients can retry. Default: `120000`; set `0` to disable. |
|
|
469
479
|
| `HOOPILOT_NO_UPDATE_CHECK` / `--no-update-check` | Disable background update checks. `NO_UPDATE_NOTIFIER` is also honored. |
|
|
470
480
|
|
|
481
|
+
The default service profile is low-resource:
|
|
482
|
+
|
|
483
|
+
```sh
|
|
484
|
+
hoopilot
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
For the quietest local service, also use `HOOPILOT_LOG_LEVEL=warn HOOPILOT_NO_UPDATE_CHECK=1`.
|
|
488
|
+
|
|
489
|
+
If you prefer fewer stream timers and can tolerate clients handling stalled upstream streams themselves, also set `HOOPILOT_UPSTREAM_STREAM_IDLE_TIMEOUT_MS=0`.
|
|
490
|
+
|
|
471
491
|
`codexx` settings:
|
|
472
492
|
|
|
473
493
|
| Setting | Description |
|
|
@@ -107,12 +107,38 @@ var STREAMING_PROXY_MODES = [
|
|
|
107
107
|
"buffer",
|
|
108
108
|
"live"
|
|
109
109
|
];
|
|
110
|
+
var USAGE_ACCOUNTING_MODES = [
|
|
111
|
+
"basic",
|
|
112
|
+
"full",
|
|
113
|
+
"off"
|
|
114
|
+
];
|
|
110
115
|
function parseStreamingProxyMode(value) {
|
|
111
116
|
if (STREAMING_PROXY_MODES.includes(value)) {
|
|
112
117
|
return value;
|
|
113
118
|
}
|
|
114
119
|
throw new Error(`Invalid stream mode: ${value}. Expected ${STREAMING_PROXY_MODES.join(", ")}.`);
|
|
115
120
|
}
|
|
121
|
+
function parseUsageAccountingMode(value) {
|
|
122
|
+
if (USAGE_ACCOUNTING_MODES.includes(value)) {
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Invalid usage accounting mode: ${value}. Expected ${USAGE_ACCOUNTING_MODES.join(", ")}.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
function parseBooleanEnv(value, name) {
|
|
130
|
+
const raw = envValue(value)?.toLowerCase();
|
|
131
|
+
if (raw === void 0) {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
if (raw === "1" || raw === "true" || raw === "yes" || raw === "on") {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
if (raw === "0" || raw === "false" || raw === "no" || raw === "off") {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`${name} must be one of: 1, 0, true, false, yes, no, on, off.`);
|
|
141
|
+
}
|
|
116
142
|
|
|
117
143
|
// src/codexx.ts
|
|
118
144
|
var DEFAULT_BASE_URL = "http://127.0.0.1:4141/v1";
|
|
@@ -313,9 +339,11 @@ export {
|
|
|
313
339
|
parseJsonObject,
|
|
314
340
|
modelIdsFromResponse,
|
|
315
341
|
parseStreamingProxyMode,
|
|
342
|
+
parseUsageAccountingMode,
|
|
343
|
+
parseBooleanEnv,
|
|
316
344
|
DEFAULT_MODEL,
|
|
317
345
|
buildCodexxInvocation,
|
|
318
346
|
main,
|
|
319
347
|
verifyCodexxModel
|
|
320
348
|
};
|
|
321
|
-
//# sourceMappingURL=chunk-
|
|
349
|
+
//# sourceMappingURL=chunk-FH6WSFOC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/codexx.ts","../src/defaults.ts","../src/util.ts"],"sourcesContent":["#!/usr/bin/env bun\n\nimport { spawn } from \"node:child_process\";\nimport { constants as osConstants } from \"node:os\";\nimport { DEFAULT_MODEL } from \"./defaults\";\nimport type { FetchLike } from \"./types\";\nimport {\n envValue,\n errorMessage,\n modelIdsFromResponse,\n trimTrailingSlash,\n truncatedResponseText,\n} from \"./util\";\n\nconst DEFAULT_BASE_URL = \"http://127.0.0.1:4141/v1\";\nconst DEFAULT_CODEX_BIN = \"codex\";\nconst DEFAULT_REASONING_EFFORT = \"xhigh\";\nconst DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000;\nconst PROXY_ENV_KEYS = [\n \"ALL_PROXY\",\n \"HTTPS_PROXY\",\n \"HTTP_PROXY\",\n \"NO_PROXY\",\n \"all_proxy\",\n \"https_proxy\",\n \"http_proxy\",\n \"no_proxy\",\n];\n\nexport interface CodexxInvocation {\n args: string[];\n baseUrl: string;\n command: string;\n env: NodeJS.ProcessEnv;\n model: string;\n}\n\nexport function buildCodexxInvocation(\n argv: string[],\n env: NodeJS.ProcessEnv = process.env,\n): CodexxInvocation {\n const baseUrl = envValue(env.CODEXX_BASE_URL) ?? DEFAULT_BASE_URL;\n // Never fall back to a public, predictable key: a shared constant like the old\n // \"local-key\" default is also a credential a malicious local/browser client\n // could guess. When no key is configured the local server is expected to run\n // unauthenticated, which accepts any value, so a random throwaway key is safe.\n const apiKey =\n envValue(env.CODEXX_API_KEY) ?? envValue(env.HOOPILOT_API_KEY) ?? generateEphemeralApiKey();\n const command = envValue(env.CODEXX_CODEX_BIN) ?? DEFAULT_CODEX_BIN;\n const model = envValue(env.CODEXX_MODEL) ?? DEFAULT_MODEL;\n const reasoningEffort = envValue(env.CODEXX_MODEL_REASONING_EFFORT) ?? DEFAULT_REASONING_EFFORT;\n const streamIdleTimeoutMs = parseStreamIdleTimeoutMs(env.CODEXX_STREAM_IDLE_TIMEOUT_MS);\n const providerConfigParts = [\n '{ name = \"Hoopilot\"',\n `base_url = ${JSON.stringify(baseUrl)}`,\n 'env_key = \"OPENAI_API_KEY\"',\n 'wire_api = \"responses\"',\n \"supports_websockets = false\",\n ];\n if (streamIdleTimeoutMs > 0) {\n providerConfigParts.push(`stream_idle_timeout_ms = ${streamIdleTimeoutMs}`);\n }\n const providerConfig = `${providerConfigParts.join(\", \")} }`;\n\n return {\n args: [\n // Codex ships a managed network proxy (codex-rs/network-proxy) that routes the\n // agent's traffic through a local proxy on :3128 and enforces a domain allowlist.\n // A host that is not allowlisted — like the local Hoopilot server — gets an instant\n // 403 (the Squid error page) and never reaches Hoopilot. It has two independent\n // gates: the `network_proxy` feature flag and the `permissions.workspace.network`\n // config. Disabling only the feature (`--disable network_proxy`) does not reliably\n // turn it off when the proxy is enabled through the permissions config, so set both\n // off: the feature flag via `--disable`, and the proxy itself via the config key\n // (when `enabled` is false the proxy no-ops and binds no listeners).\n \"--disable\",\n \"network_proxy\",\n \"-c\",\n \"permissions.workspace.network.enabled=false\",\n \"-c\",\n 'model_provider=\"hoopilot\"',\n \"-c\",\n `model_providers.hoopilot=${providerConfig}`,\n \"-m\",\n model,\n \"-c\",\n `model_reasoning_effort=${JSON.stringify(reasoningEffort)}`,\n ...argv,\n ],\n baseUrl,\n command,\n env: withoutProxyEnv({\n ...env,\n OPENAI_API_KEY: apiKey,\n }),\n model,\n };\n}\n\n// A random, non-guessable placeholder key for when neither CODEXX_API_KEY nor\n// HOOPILOT_API_KEY is set. An unauthenticated local Hoopilot accepts any value;\n// a keyed server rejects it with a 401, which the model preflight surfaces.\nfunction generateEphemeralApiKey(): string {\n return `codexx-${crypto.randomUUID()}`;\n}\n\nfunction parseStreamIdleTimeoutMs(rawValue: string | undefined): number {\n const raw = envValue(rawValue);\n if (raw === undefined) {\n return DEFAULT_STREAM_IDLE_TIMEOUT_MS;\n }\n const value = Number(raw);\n if (!Number.isInteger(value) || value < 0) {\n throw new Error(\"CODEXX_STREAM_IDLE_TIMEOUT_MS must be a non-negative integer.\");\n }\n return value;\n}\n\nfunction withoutProxyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const next = { ...env };\n for (const key of PROXY_ENV_KEYS) {\n delete next[key];\n }\n return next;\n}\n\nexport async function main(argv = Bun.argv.slice(2), env = process.env): Promise<void> {\n if (argv.length === 1 && (argv[0] === \"--help\" || argv[0] === \"-h\")) {\n console.log(helpText());\n return;\n }\n\n const invocation = buildCodexxInvocation(argv, env);\n if (env.CODEXX_SKIP_MODEL_PREFLIGHT !== \"1\") {\n await verifyCodexxModel(invocation);\n }\n const child = spawn(invocation.command, invocation.args, {\n env: invocation.env,\n shell: process.platform === \"win32\",\n stdio: \"inherit\",\n });\n\n const exitCode = await new Promise<number>((resolve, reject) => {\n child.once(\"error\", reject);\n child.once(\"exit\", (code, signal) => {\n if (typeof code === \"number\") {\n resolve(code);\n return;\n }\n resolve(signal ? 128 + signalNumber(signal) : 1);\n });\n });\n\n process.exitCode = exitCode;\n}\n\nexport async function verifyCodexxModel(\n invocation: Pick<CodexxInvocation, \"baseUrl\" | \"env\" | \"model\">,\n fetcher: FetchLike = fetch,\n): Promise<void> {\n const modelsUrl = `${trimTrailingSlash(invocation.baseUrl)}/models`;\n const apiKey = invocation.env.OPENAI_API_KEY;\n if (apiKey === undefined) {\n throw new Error(\n \"verifyCodexxModel requires invocation.env.OPENAI_API_KEY; build the invocation with buildCodexxInvocation.\",\n );\n }\n let response: Response;\n try {\n response = await fetcher(modelsUrl, {\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${apiKey}`,\n },\n method: \"GET\",\n });\n } catch (error) {\n throw new Error(\n `Could not reach Hoopilot at ${modelsUrl}. Start Hoopilot first, or set CODEXX_SKIP_MODEL_PREFLIGHT=1 to skip this check. ${errorMessage(error)}`,\n );\n }\n\n if (!response.ok) {\n throw new Error(\n `Could not verify model ${JSON.stringify(invocation.model)} because ${modelsUrl} returned ${response.status}: ${await truncatedResponseText(response)}`,\n );\n }\n\n const models = modelIdsFromResponse(await response.json().catch(() => undefined));\n if (models.length > 0 && !models.includes(invocation.model)) {\n throw new Error(\n `The logged-in Copilot account does not advertise model ${JSON.stringify(invocation.model)} at ${modelsUrl}. Available models: ${models.join(\", \")}. After upgrading Hoopilot, rerun \"hoopilot login\" to refresh the Copilot OAuth token, or set CODEXX_MODEL to one of the advertised model IDs.`,\n );\n }\n}\n\nfunction helpText(): string {\n return `codexx\n\nRun Codex against an already-running local Hoopilot server.\n\nUsage:\n codexx [codex options] [prompt]\n\nEnvironment:\n CODEXX_BASE_URL OpenAI-compatible base URL. Default: ${DEFAULT_BASE_URL}\n CODEXX_API_KEY API key sent to the local Hoopilot server.\n HOOPILOT_API_KEY Used as the API key when CODEXX_API_KEY is unset. When\n neither is set, a random throwaway key is generated for\n an unauthenticated local server.\n CODEXX_CODEX_BIN Codex executable to run. Default: ${DEFAULT_CODEX_BIN}\n CODEXX_MODEL Codex model to use. Default: ${DEFAULT_MODEL}\n CODEXX_MODEL_REASONING_EFFORT\n Codex reasoning effort. Default: ${DEFAULT_REASONING_EFFORT}\n CODEXX_STREAM_IDLE_TIMEOUT_MS\n Codex Responses stream idle timeout in milliseconds. Default:\n ${DEFAULT_STREAM_IDLE_TIMEOUT_MS}; set 0 to use Codex's own default.\n CODEXX_SKIP_MODEL_PREFLIGHT\n Set to 1 to skip checking /v1/models before starting Codex.\n\ncodexx does not start Hoopilot and does not change your shell environment. It selects a temporary Hoopilot model provider with Responses WebSockets disabled, uses ${DEFAULT_MODEL} with ${DEFAULT_REASONING_EFFORT} reasoning by default, disables Codex's managed network proxy (permissions.workspace.network.enabled=false) so requests reach the local server instead of being blocked by its allowlist, and removes proxy variables only from the spawned Codex process.`;\n}\n\nfunction signalNumber(signal: NodeJS.Signals): number {\n return osConstants.signals[signal] ?? 1;\n}\n\nif (import.meta.main) {\n main().catch((error: unknown) => {\n console.error(errorMessage(error));\n process.exit(1);\n });\n}\n","/** Default model Hoopilot uses when a client does not supply one. */\nexport const DEFAULT_MODEL = \"gpt-5.5\";\n","import type { JsonObject, StreamingProxyMode, UsageAccountingMode } from \"./types\";\n\n/** Remove any trailing slashes from a URL or path string. */\nexport function trimTrailingSlash(value: string): string {\n return value.replace(/\\/+$/, \"\");\n}\n\n/** Treat blank environment variables as unset while preserving nonblank values. */\nexport function envValue(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\n/** True for HTTPS URLs, or HTTP only on loopback hosts used by local tests/dev. */\nexport function isHttpsOrLoopbackUrl(rawUrl: string): boolean {\n const url = parseUrl(rawUrl);\n if (!url) {\n return false;\n }\n return url.protocol === \"https:\" || isLoopbackHttpUrl(url);\n}\n\n/** Validate a base URL before sending a bearer/OAuth token to it. */\nexport function isTrustedTokenBaseUrl(\n rawUrl: string,\n allowedHttpsHosts: readonly string[],\n allowUnsafeHttps = false,\n): boolean {\n const url = parseUrl(rawUrl);\n if (!url) {\n return false;\n }\n if (url.username || url.password || url.search || url.hash) {\n return false;\n }\n if (url.pathname !== \"\" && url.pathname !== \"/\") {\n return false;\n }\n if (isLoopbackHttpUrl(url)) {\n return true;\n }\n if (url.protocol !== \"https:\") {\n return false;\n }\n const host = url.hostname.toLowerCase();\n return allowedHttpsHosts.includes(host) || allowUnsafeHttps;\n}\n\nfunction parseUrl(rawUrl: string): URL | undefined {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n return undefined;\n }\n return url;\n}\n\nconst LOOPBACK_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\"]);\n\n/** True for hostnames that always resolve to the local machine. */\nexport function isLoopbackHostname(host: string): boolean {\n return LOOPBACK_HOSTNAMES.has(host);\n}\n\nfunction isLoopbackHttpUrl(url: URL): boolean {\n return url.protocol === \"http:\" && isLoopbackHostname(url.hostname);\n}\n\n/** Read a response body as text, truncated to keep error messages bounded. */\nexport async function truncatedResponseText(response: Response, max = 500): Promise<string> {\n const text = await response.text();\n return text.slice(0, max);\n}\n\n/** Narrow an unknown value to a plain object, returning {} for arrays/primitives/null. */\nexport function asRecord(value: unknown): JsonObject {\n return value && typeof value === \"object\" && !Array.isArray(value) ? (value as JsonObject) : {};\n}\n\n/** Extract a human-readable message from an unknown thrown value. */\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Return the first finite number among the candidates, else undefined. */\nexport function firstNumber(...values: unknown[]): number | undefined {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n }\n return undefined;\n}\n\n/** Generate a dash-free random identifier for synthesized response/message ids. */\nexport function randomId(): string {\n return crypto.randomUUID().replaceAll(\"-\", \"\");\n}\n\n/** Drop keys whose value is undefined so they are omitted from JSON output. */\nexport function removeUndefined<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\n/** Parse JSON, returning undefined instead of throwing on malformed input. */\nexport function safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n\n/** Parse JSON into a plain object, returning undefined on malformed or non-object input. */\nexport function parseJsonObject(text: string): JsonObject | undefined {\n try {\n return asRecord(JSON.parse(text));\n } catch {\n return undefined;\n }\n}\n\n/**\n * Extract de-duplicated model IDs from an OpenAI-style `/models` response (an\n * object carrying a `data` array, or a bare array of model objects).\n */\nexport function modelIdsFromResponse(body: unknown): string[] {\n const record = asRecord(body);\n const data = Array.isArray(record.data) ? record.data : Array.isArray(body) ? body : [];\n const seen = new Set<string>();\n const ids: string[] = [];\n for (const model of data) {\n const id = asRecord(model).id;\n if (typeof id !== \"string\" || id.length === 0 || seen.has(id)) {\n continue;\n }\n seen.add(id);\n ids.push(id);\n }\n return ids;\n}\n\n/** Canonical set of accepted streaming-proxy modes, kept in sync with {@link StreamingProxyMode}. */\nexport const STREAMING_PROXY_MODES = [\n \"auto\",\n \"buffer\",\n \"live\",\n] as const satisfies readonly StreamingProxyMode[];\n\n/** Canonical set of accepted token/accounting modes, kept in sync with {@link UsageAccountingMode}. */\nexport const USAGE_ACCOUNTING_MODES = [\n \"basic\",\n \"full\",\n \"off\",\n] as const satisfies readonly UsageAccountingMode[];\n\n/** Validate a stream-mode string against the allowed {@link StreamingProxyMode} values. */\nexport function parseStreamingProxyMode(value: string): StreamingProxyMode {\n if ((STREAMING_PROXY_MODES as readonly string[]).includes(value)) {\n return value as StreamingProxyMode;\n }\n throw new Error(`Invalid stream mode: ${value}. Expected ${STREAMING_PROXY_MODES.join(\", \")}.`);\n}\n\n/** Validate a usage-accounting string against the allowed {@link UsageAccountingMode} values. */\nexport function parseUsageAccountingMode(value: string): UsageAccountingMode {\n if ((USAGE_ACCOUNTING_MODES as readonly string[]).includes(value)) {\n return value as UsageAccountingMode;\n }\n throw new Error(\n `Invalid usage accounting mode: ${value}. Expected ${USAGE_ACCOUNTING_MODES.join(\", \")}.`,\n );\n}\n\n/** Parse common environment boolean spellings. */\nexport function parseBooleanEnv(value: string | undefined, name: string): boolean | undefined {\n const raw = envValue(value)?.toLowerCase();\n if (raw === undefined) {\n return undefined;\n }\n if (raw === \"1\" || raw === \"true\" || raw === \"yes\" || raw === \"on\") {\n return true;\n }\n if (raw === \"0\" || raw === \"false\" || raw === \"no\" || raw === \"off\") {\n return false;\n }\n throw new Error(`${name} must be one of: 1, 0, true, false, yes, no, on, off.`);\n}\n"],"mappings":"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","names":[]}
|