pi-spark 0.19.2 → 0.20.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/README.md CHANGED
@@ -120,14 +120,33 @@ All fields are optional. Each top-level feature runs with the defaults below unl
120
120
 
121
121
  | Field | Value (or `false`) | Description |
122
122
  | --- | --- | --- |
123
- | `credits` | `{}` | Shows the active provider's credit balance or rate-limit usage in the status line. |
123
+ | `credits` | `CreditsConfig` | Shows the active provider's credit balance or rate-limit usage in the status line. |
124
124
  | `editor` | `EditorConfig` | Shows a working indicator and the current model on the editor's top border. |
125
- | `footer` | `{}` | Shows session info, extension statuses, cost, and context usage on one line. |
125
+ | `footer` | `FooterConfig` | Shows session info, extension statuses, cost, and context usage. |
126
126
  | `fullscreen` | `{}` | Clears the screen and scrollback on start and exit, and pins the editor and footer to the bottom. |
127
127
  | `presets` | `{ [name]: Preset }` | Defines named model presets, keyed by name. |
128
128
  | `recap` | `RecapConfig` | Generates a session recap when idle or on demand. |
129
129
  | `title` | `TitleConfig` | Names the session automatically after the first exchange. |
130
130
 
131
+ #### `CreditsConfig`
132
+
133
+ All supported providers are enabled by default. Set a provider to `false` to disable its credits status, or to `true` to override a global `false` in project config.
134
+
135
+ ```json
136
+ {
137
+ "credits": {
138
+ "providers": {
139
+ "fireworks": false,
140
+ "openai-codex": true
141
+ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ | Field | Value | Description |
147
+ | --- | --- | --- |
148
+ | `providers` | partial map of provider IDs to booleans | Enables or disables credits for individual providers. Valid IDs: `deepseek`, `fireworks`, `kimi-coding`, `moonshotai`, `moonshotai-cn`, `openai-codex`, `openrouter`, and `vercel-ai-gateway`. |
149
+
131
150
  #### `EditorConfig`
132
151
 
133
152
  The `spinner` field is optional and defaults to `tildes`.
@@ -139,6 +158,15 @@ The `spinner` field is optional and defaults to `tildes`.
139
158
  | | `tildes` (default) | `∼, ≈, ≋, ≈, ∼` |
140
159
  | | `pulse` | `·, •, ●, •, ·` |
141
160
 
161
+ #### `FooterConfig`
162
+
163
+ The `statusPosition` field is optional and defaults to `inline`.
164
+
165
+ | Field | Value | Description |
166
+ | --- | --- | --- |
167
+ | `statusPosition` | `inline` (default) | Shows extension statuses on the same line as session info, cost, and context usage. |
168
+ | | `below` | Moves extension statuses to a new line below. |
169
+
142
170
  #### `Preset`
143
171
 
144
172
  Each preset must set all three fields.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-spark",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "description": "Pi package that polishes your daily experience.",
5
5
  "keywords": [
6
6
  "pi-coding-agent",
@@ -1,3 +1,20 @@
1
1
  import * as z from "zod";
2
2
 
3
- export const creditsConfigSchema = z.object({});
3
+ const creditsProviderIdSchema = z.enum([
4
+ "deepseek",
5
+ "fireworks",
6
+ "kimi-coding",
7
+ "moonshotai",
8
+ "moonshotai-cn",
9
+ "openai-codex",
10
+ "openrouter",
11
+ "vercel-ai-gateway",
12
+ ]);
13
+
14
+ export type CreditsProviderId = z.infer<typeof creditsProviderIdSchema>;
15
+
16
+ export const creditsConfigSchema = z.object({
17
+ providers: z.partialRecord(creditsProviderIdSchema, z.boolean()).optional(),
18
+ });
19
+
20
+ export type CreditsConfig = z.infer<typeof creditsConfigSchema>;
@@ -1,5 +1,5 @@
1
1
  import { CreditsManager } from "./manager";
2
- import { registerProviderExtensions } from "./providers";
2
+ import { getEnabledProviders, registerProviderExtensions } from "./providers";
3
3
  import { loadConfig } from "../../config";
4
4
  import { isUsage } from "../../utils/usage";
5
5
 
@@ -20,8 +20,11 @@ export function registerCredits(pi: ExtensionAPI): void {
20
20
  const config = loadConfig(ctx).credits;
21
21
  if (!ctx.hasUI || !config) return;
22
22
 
23
- creditsManager = new CreditsManager();
24
- registerProviderExtensions(pi, ctx, async (ctx) => await creditsManager?.refresh(ctx));
23
+ const providers = getEnabledProviders(config);
24
+ if (providers.length === 0) return;
25
+
26
+ creditsManager = new CreditsManager(providers);
27
+ registerProviderExtensions(pi, ctx, providers, async (ctx) => await creditsManager?.refresh(ctx));
25
28
 
26
29
  creditsManager.refresh(ctx);
27
30
  });
@@ -9,12 +9,17 @@ const REQUEST_TIMEOUT_MS = 30_000;
9
9
 
10
10
  export class CreditsManager {
11
11
  private inflight: AbortController | undefined = undefined;
12
+ private providers: CreditsProvider[];
12
13
  private currentProvider: string | undefined = undefined;
13
14
 
15
+ constructor(providers: CreditsProvider[]) {
16
+ this.providers = providers;
17
+ }
18
+
14
19
  async refresh(ctx: ExtensionContext): Promise<void> {
15
20
  this.inflight?.abort();
16
21
 
17
- const provider = findProvider(ctx.model?.provider);
22
+ const provider = findProvider(this.providers, ctx.model?.provider);
18
23
  if (!provider) {
19
24
  this.inflight = undefined;
20
25
  this.currentProvider = undefined;
@@ -7,6 +7,7 @@ import { openrouterProvider } from "./openrouter";
7
7
  import { vercelAiGatewayProvider } from "./vercel-ai-gateway";
8
8
 
9
9
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import type { CreditsConfig } from "../config";
10
11
  import type { CreditsProvider, RefreshCredits } from "../types";
11
12
 
12
13
  const PROVIDERS: CreditsProvider[] = [
@@ -20,10 +21,15 @@ const PROVIDERS: CreditsProvider[] = [
20
21
  vercelAiGatewayProvider,
21
22
  ];
22
23
 
23
- export function findProvider(provider?: string): CreditsProvider | undefined {
24
- return PROVIDERS.find((entry) => entry.id === provider);
24
+ export function getEnabledProviders(config: CreditsConfig): CreditsProvider[] {
25
+ const settings = config.providers as Partial<Record<string, boolean>> | undefined;
26
+ return PROVIDERS.filter((provider) => settings?.[provider.id] !== false);
25
27
  }
26
28
 
27
- export function registerProviderExtensions(pi: ExtensionAPI, ctx: ExtensionContext, refresh: RefreshCredits): void {
28
- for (const provider of PROVIDERS) provider.register?.(pi, ctx, refresh);
29
+ export function findProvider(providers: CreditsProvider[], provider?: string): CreditsProvider | undefined {
30
+ return providers.find((entry) => entry.id === provider);
31
+ }
32
+
33
+ export function registerProviderExtensions(pi: ExtensionAPI, ctx: ExtensionContext, providers: CreditsProvider[], refresh: RefreshCredits): void {
34
+ for (const provider of providers) provider.register?.(pi, ctx, refresh);
29
35
  }
@@ -4,18 +4,45 @@ import type { Credits, CreditsLane, CreditsProvider } from "../types";
4
4
 
5
5
  const PROVIDER = "kimi-coding";
6
6
  const URL = "https://api.kimi.com/coding/v1/usages";
7
+ const BOOSTER_FIXED_POINT_CENTS = 1_000_000;
8
+
9
+ interface KimiCodeUsage {
10
+ limit?: string | number;
11
+ used?: string | number;
12
+ remaining?: string | number;
13
+ }
14
+
15
+ interface KimiCodeLimit {
16
+ window?: { duration?: number; timeUnit?: string } | null;
17
+ detail?: KimiCodeUsage | null;
18
+ }
19
+
20
+ interface KimiCodeBoosterWallet {
21
+ balance?: {
22
+ type?: string;
23
+ amount?: string | number;
24
+ amountLeft?: string | number;
25
+ } | null;
26
+ }
7
27
 
8
28
  interface KimiCodeUsageResponse {
9
- usage?: { used?: string | number } | null;
10
- limits?: {
11
- window?: { duration?: number; timeUnit?: string } | null;
12
- detail?: { used?: string | number } | null;
13
- }[] | null;
29
+ usage?: KimiCodeUsage | null;
30
+ limits?: KimiCodeLimit[] | null;
31
+ boosterWallet?: KimiCodeBoosterWallet | null;
14
32
  }
15
33
 
16
- function toPercent(used: number | undefined): number | undefined {
17
- if (used === undefined) return undefined;
18
- return Math.min(100, Math.max(0, used));
34
+ function toPercent(detail: KimiCodeUsage): number | undefined {
35
+ const limit = toNumber(detail.limit);
36
+ let used = toNumber(detail.used);
37
+
38
+ if (used === undefined && limit !== undefined) {
39
+ const remaining = toNumber(detail.remaining);
40
+ if (remaining !== undefined) used = limit - remaining;
41
+ }
42
+
43
+ if (used === undefined || limit === undefined || limit <= 0) return undefined;
44
+
45
+ return Math.min(100, Math.max(0, (used / limit) * 100));
19
46
  }
20
47
 
21
48
  function formatWindowLabel(window?: { duration?: number; timeUnit?: string } | null): string {
@@ -34,8 +61,20 @@ function formatWindowLabel(window?: { duration?: number; timeUnit?: string } | n
34
61
  return `${duration}${unit.replace("TIME_UNIT_", "").toLowerCase()}`;
35
62
  }
36
63
 
37
- function buildLane(label: string, detail?: KimiCodeUsageResponse["usage"]): CreditsLane {
38
- return { label, percent: toPercent(toNumber(detail?.used)) };
64
+ function buildLane(label: string, detail: KimiCodeUsage): CreditsLane | undefined {
65
+ const percent = toPercent(detail);
66
+ return percent === undefined ? undefined : { label, percent };
67
+ }
68
+
69
+ function formatSuffix(wallet?: KimiCodeBoosterWallet | null): string | undefined {
70
+ if (wallet?.balance?.type !== "BOOSTER") return undefined;
71
+
72
+ const amount = toNumber(wallet.balance.amount);
73
+ if (amount === undefined || amount <= 0) return undefined;
74
+
75
+ const amountLeft = Math.max(0, toNumber(wallet.balance.amountLeft) ?? 0);
76
+ const remaining = Math.round(amountLeft / BOOSTER_FIXED_POINT_CENTS) / 100;
77
+ return `(Extra Usage $${remaining.toFixed(2)})`;
39
78
  }
40
79
 
41
80
  export const kimiCodeProvider: CreditsProvider = {
@@ -55,15 +94,19 @@ export const kimiCodeProvider: CreditsProvider = {
55
94
  const lanes: CreditsLane[] = [];
56
95
 
57
96
  payload.limits?.forEach((item) => {
58
- if (!item) return;
97
+ if (!item.detail) return;
59
98
 
60
- const label = formatWindowLabel(item.window);
61
- lanes.push(buildLane(label, item.detail));
99
+ const lane = buildLane(formatWindowLabel(item.window), item.detail);
100
+ if (lane) lanes.push(lane);
62
101
  });
63
102
 
64
- const weeklyLane = buildLane("7d", payload.usage);
65
- lanes.push(weeklyLane);
103
+ if (payload.usage) {
104
+ const weeklyLane = buildLane("7d", payload.usage);
105
+ if (weeklyLane) lanes.push(weeklyLane);
106
+ }
107
+
108
+ if (lanes.length === 0) throw new Error("no usage data");
66
109
 
67
- return { type: "windows", lanes };
110
+ return { type: "windows", lanes, suffix: formatSuffix(payload.boosterWallet) };
68
111
  },
69
112
  };
@@ -13,7 +13,7 @@ interface MoonshotBalanceResponse {
13
13
  * different currencies (USD vs CNY), which the endpoint does not report, so each Pi provider ID
14
14
  * fixes both host and currency.
15
15
  */
16
- function createMoonshotProvider(id: string, host: string, currency: string): CreditsProvider {
16
+ function createMoonshotProvider(id: "moonshotai" | "moonshotai-cn", host: string, currency: string): CreditsProvider {
17
17
  return {
18
18
  id,
19
19
  label: "Moonshot",
@@ -1,5 +1,5 @@
1
- import type { ProviderId } from "@earendil-works/pi-ai";
2
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { CreditsProviderId } from "./config";
3
3
 
4
4
  /**
5
5
  * Normalized credits/usage for a provider.
@@ -21,7 +21,7 @@ export type RefreshCredits = (ctx: ExtensionContext) => Promise<void>;
21
21
 
22
22
  /** A credits source for a Pi provider, shown in the status line while that provider is active. */
23
23
  export interface CreditsProvider {
24
- readonly id: ProviderId;
24
+ readonly id: CreditsProviderId;
25
25
  readonly label: string;
26
26
  fetch(apiKey: string, signal: AbortSignal): Promise<Credits>;
27
27
  register?(pi: ExtensionAPI, ctx: ExtensionContext, refresh: RefreshCredits): void;
@@ -1,6 +1,8 @@
1
1
  import * as z from "zod";
2
2
 
3
- import { spinnerPresetSchema } from "./spinner";
3
+ const spinnerPresetSchema = z.enum(["dots", "lights", "tildes", "pulse"]);
4
+
5
+ export type SpinnerPreset = z.infer<typeof spinnerPresetSchema>;
4
6
 
5
7
  export const editorConfigSchema = z.object({
6
8
  spinner: spinnerPresetSchema.optional(),
@@ -1,10 +1,5 @@
1
- import * as z from "zod";
2
-
3
1
  import type { TUI } from "@earendil-works/pi-tui";
4
-
5
- export const spinnerPresetSchema = z.enum(["dots", "lights", "tildes", "pulse"]);
6
-
7
- type SpinnerPreset = z.infer<typeof spinnerPresetSchema>;
2
+ import type { SpinnerPreset } from "./config";
8
3
 
9
4
  interface SpinnerParams {
10
5
  frames: string[];
@@ -35,7 +30,7 @@ const SPINNER_PRESETS: Record<SpinnerPreset, SpinnerParams> = {
35
30
  },
36
31
  };
37
32
 
38
- const DEFAULT_SPINNER_PRESET = "tildes";
33
+ const DEFAULT_SPINNER_PRESET: SpinnerPreset = "tildes";
39
34
 
40
35
  export class Spinner {
41
36
  private tui: TUI | undefined;
@@ -1,3 +1,9 @@
1
1
  import * as z from "zod";
2
2
 
3
- export const footerConfigSchema = z.object({});
3
+ const statusPositionSchema = z.enum(["inline", "below"]);
4
+
5
+ export type StatusPosition = z.infer<typeof statusPositionSchema>;
6
+
7
+ export const footerConfigSchema = z.object({
8
+ statusPosition: statusPositionSchema.optional(),
9
+ });
@@ -1,27 +1,30 @@
1
1
  import { homedir } from "node:os";
2
2
  import { resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
+ import { hyperlink, Text } from "@earendil-works/pi-tui";
4
5
 
5
6
  import { SplitLine } from "../../components/split-line";
6
7
  import { loadConfig } from "../../config";
7
8
  import { formatContextUsage, formatCost, formatCwd, sanitizeText } from "../../utils/format";
8
- import { isFreeModel } from "../../utils/model";
9
9
  import { getEntryUsage } from "../../utils/usage";
10
10
 
11
- import { hyperlink } from "@earendil-works/pi-tui";
12
-
13
11
  import type { ExtensionContext, ExtensionAPI, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
14
12
  import type { Component } from "@earendil-works/pi-tui";
13
+ import type { StatusPosition } from "./config";
14
+
15
+ const DEFAULT_STATUS_POSITION: StatusPosition = "inline";
15
16
 
16
17
  class FooterComponent implements Component {
17
18
  private ctx: ExtensionContext;
18
19
  private theme: Theme;
19
20
  private footerData: ReadonlyFooterDataProvider;
21
+ private statusPosition: StatusPosition;
20
22
 
21
- constructor(ctx: ExtensionContext, theme: Theme, footerData: ReadonlyFooterDataProvider) {
23
+ constructor(ctx: ExtensionContext, theme: Theme, footerData: ReadonlyFooterDataProvider, statusPosition: StatusPosition = DEFAULT_STATUS_POSITION) {
22
24
  this.ctx = ctx;
23
25
  this.theme = theme;
24
26
  this.footerData = footerData;
27
+ this.statusPosition = statusPosition;
25
28
  }
26
29
 
27
30
  invalidate(): void {
@@ -32,7 +35,14 @@ class FooterComponent implements Component {
32
35
  const left = this.getLeft();
33
36
  const right = this.getRight();
34
37
 
35
- return new SplitLine(left, right, { primarySide: "right", ellipsis: this.theme.fg("dim", "…") }).render(width);
38
+ const lines = new SplitLine(left, right, { primarySide: "right", ellipsis: this.theme.fg("dim", "…") }).render(width);
39
+
40
+ if (this.statusPosition === "below") {
41
+ const statusesText = this.getStatusesText();
42
+ if (statusesText) lines.push(...new Text(statusesText, 0, 0).render(width));
43
+ }
44
+
45
+ return lines;
36
46
  }
37
47
 
38
48
  private getLeft(): string {
@@ -46,7 +56,7 @@ class FooterComponent implements Component {
46
56
  }
47
57
 
48
58
  private getRight(): string {
49
- const statusesText = this.getStatusesText();
59
+ const statusesText = this.statusPosition === "inline" ? this.getStatusesText() : "";
50
60
  const styledCostText = this.getStyledCostText();
51
61
  const styledContextUsageText = this.getStyledContextUsageText();
52
62
 
@@ -67,8 +77,8 @@ class FooterComponent implements Component {
67
77
  private getStyledCostText(): string {
68
78
  const totalCost = this.ctx.sessionManager.getBranch().reduce((acc, entry) => acc + (getEntryUsage(entry)?.cost.total ?? 0), 0);
69
79
 
70
- // Hide cost when it's zero and the current model is free.
71
- if (totalCost === 0 && this.ctx.model && isFreeModel(this.ctx.model)) return "";
80
+ // Hide cost below half a cent, since it would render as $0.00.
81
+ if (totalCost < 0.005) return "";
72
82
 
73
83
  const costText = formatCost(totalCost);
74
84
 
@@ -92,6 +102,6 @@ export function registerFooter(pi: ExtensionAPI): void {
92
102
  const config = loadConfig(ctx).footer;
93
103
  if (!ctx.hasUI || !config) return;
94
104
 
95
- ctx.ui.setFooter((_tui, theme, footerData) => new FooterComponent(ctx, theme, footerData));
105
+ ctx.ui.setFooter((_tui, theme, footerData) => new FooterComponent(ctx, theme, footerData, config.statusPosition));
96
106
  });
97
107
  }
@@ -7,11 +7,53 @@ import type { Component, TUI } from "@earendil-works/pi-tui";
7
7
  export class BottomFiller implements Component {
8
8
  private tui: TUI;
9
9
  private measuring = false;
10
+ private statusContainer?: Component;
11
+ private originalStatusRender?: Component["render"];
12
+ private patchedStatusRender?: Component["render"];
10
13
 
11
14
  constructor(tui: TUI) {
12
15
  this.tui = tui;
13
16
  }
14
17
 
18
+ /**
19
+ * Suppress Pi's two-line idle status placeholder while leaving active status
20
+ * indicators and `clearOnShrink` unchanged.
21
+ *
22
+ * Pi added the placeholder to prevent status indicators from shrinking the TUI in
23
+ * [#6026](https://github.com/earendil-works/pi/pull/6026). Fullscreen already stabilizes short
24
+ * layouts with this filler, while in long layouts the placeholder surfaces as blank space above
25
+ * the editor. This render wrapper specifically counteracts that placeholder without hiding real
26
+ * statuses.
27
+ */
28
+ suppressIdleStatus(): void {
29
+ if (this.patchedStatusRender) return;
30
+
31
+ const widgetIndex = this.tui.children.findIndex((child) => "children" in child && Array.isArray(child.children) && child.children.includes(this));
32
+ if (widgetIndex <= 0) return;
33
+
34
+ const statusContainer = this.tui.children[widgetIndex - 1];
35
+ if (!statusContainer) return;
36
+
37
+ const originalRender = statusContainer.render;
38
+ const patchedRender = (width: number): string[] => {
39
+ const lines = originalRender.call(statusContainer, width);
40
+ return lines.length === 2 && lines.every((line) => line === " ".repeat(width)) ? [] : lines;
41
+ };
42
+
43
+ statusContainer.render = patchedRender;
44
+ this.statusContainer = statusContainer;
45
+ this.originalStatusRender = originalRender;
46
+ this.patchedStatusRender = patchedRender;
47
+ }
48
+
49
+ dispose(): void {
50
+ const statusContainer = this.statusContainer;
51
+ const originalStatusRender = this.originalStatusRender;
52
+ if (statusContainer && originalStatusRender && statusContainer.render === this.patchedStatusRender) {
53
+ statusContainer.render = originalStatusRender;
54
+ }
55
+ }
56
+
15
57
  invalidate(): void {
16
58
  // No-op
17
59
  }
@@ -50,14 +50,17 @@ export function registerFullscreen(pi: ExtensionAPI): void {
50
50
  ctx.ui.setWidget(WIDGET_KEY, (capturedTui) => {
51
51
  tui = capturedTui;
52
52
 
53
- // Defer the entry clear to a microtask: the filler isn't in the render tree until
54
- // `setWidget` returns, and `session_start` has no TUI to repaint with yet.
55
- if (pendingClear) {
56
- pendingClear = false;
57
- queueMicrotask(() => capturedTui.requestRender(true));
58
- }
59
-
60
- return new BottomFiller(capturedTui);
53
+ const filler = new BottomFiller(capturedTui);
54
+ const shouldClear = pendingClear;
55
+ pendingClear = false;
56
+
57
+ // Defer setup until `setWidget` has mounted the filler in the render tree.
58
+ queueMicrotask(() => {
59
+ filler.suppressIdleStatus();
60
+ if (shouldClear) capturedTui.requestRender(true);
61
+ });
62
+
63
+ return filler;
61
64
  });
62
65
  });
63
66
 
@@ -76,6 +79,7 @@ export function registerFullscreen(pi: ExtensionAPI): void {
76
79
  tui.terminal.write(`${line}\r\n`);
77
80
  }
78
81
 
82
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
79
83
  tui = undefined;
80
84
  enabled = false;
81
85
  pendingClear = false;
@@ -2,7 +2,8 @@ import * as z from "zod";
2
2
 
3
3
  import { modelSchema } from "../../config/model";
4
4
 
5
+ export type PresetConfig = z.infer<typeof modelSchema>;
6
+
5
7
  export const presetsConfigSchema = z.record(z.string().min(1), modelSchema);
6
8
 
7
9
  export type PresetsConfig = z.infer<typeof presetsConfigSchema>;
8
- export type PresetConfig = PresetsConfig[string];
@@ -2,7 +2,7 @@ import { PRESET_CHANGE } from "../../events";
2
2
  import { formatModel } from "../../utils/format";
3
3
 
4
4
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
- import type { PresetsConfig, PresetConfig } from "./config";
5
+ import type { PresetConfig, PresetsConfig } from "./config";
6
6
 
7
7
  export class PresetManager {
8
8
  private pi: ExtensionAPI;
@@ -26,11 +26,6 @@ type ThinkingLevelSelection = {
26
26
  warning: string | undefined;
27
27
  };
28
28
 
29
- /** Check if a model has zero costs for all categories. */
30
- export function isFreeModel(model: Model<Api>): boolean {
31
- return model.cost.input === 0 && model.cost.output === 0 && model.cost.cacheRead === 0 && model.cost.cacheWrite === 0;
32
- }
33
-
34
29
  /**
35
30
  * Complete a one-shot background request, using an isolated session for OpenAI Codex models.
36
31
  * See [the investigation](../../docs/background-model-calls-and-openai-codex-sessions.md).