@oh-my-pi/pi-coding-agent 16.2.6 → 16.2.7

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cli.js +2656 -2651
  3. package/dist/types/cli/bench-cli.d.ts +3 -3
  4. package/dist/types/commands/bench.d.ts +1 -1
  5. package/dist/types/config/service-tier.d.ts +39 -24
  6. package/dist/types/config/settings-schema.d.ts +36 -36
  7. package/dist/types/mcp/config-writer.d.ts +48 -0
  8. package/dist/types/mcp/types.d.ts +3 -0
  9. package/dist/types/session/agent-session.d.ts +33 -13
  10. package/dist/types/session/messages.d.ts +1 -1
  11. package/dist/types/session/session-context.d.ts +2 -2
  12. package/dist/types/session/session-entries.d.ts +2 -2
  13. package/dist/types/session/session-manager.d.ts +2 -2
  14. package/dist/types/system-prompt.test.d.ts +1 -0
  15. package/dist/types/task/executor.d.ts +6 -6
  16. package/dist/types/task/types.d.ts +6 -0
  17. package/dist/types/task/worktree.d.ts +32 -6
  18. package/dist/types/tiny/title-client.d.ts +5 -1
  19. package/dist/types/tools/index.d.ts +3 -3
  20. package/dist/types/utils/git.d.ts +17 -0
  21. package/package.json +12 -12
  22. package/src/cli/bench-cli.ts +19 -12
  23. package/src/cli/tiny-models-cli.ts +18 -4
  24. package/src/commands/bench.ts +3 -3
  25. package/src/config/mcp-schema.json +10 -1
  26. package/src/config/service-tier.ts +85 -56
  27. package/src/config/settings-schema.ts +42 -36
  28. package/src/config/settings.ts +47 -0
  29. package/src/eval/agent-bridge.ts +4 -2
  30. package/src/internal-urls/docs-index.generated.txt +1 -1
  31. package/src/main.ts +1 -1
  32. package/src/mcp/config-writer.ts +121 -0
  33. package/src/mcp/config.ts +10 -6
  34. package/src/mcp/types.ts +3 -0
  35. package/src/modes/components/extensions/extension-dashboard.ts +46 -0
  36. package/src/modes/components/extensions/state-manager.ts +24 -3
  37. package/src/modes/controllers/event-controller.ts +7 -0
  38. package/src/modes/utils/transcript-render-helpers.ts +2 -2
  39. package/src/sdk.ts +12 -11
  40. package/src/session/agent-session.ts +186 -76
  41. package/src/session/messages.ts +1 -1
  42. package/src/session/session-context.ts +4 -4
  43. package/src/session/session-entries.ts +2 -2
  44. package/src/session/session-manager.ts +9 -2
  45. package/src/slash-commands/builtin-registry.ts +2 -10
  46. package/src/system-prompt.test.ts +158 -0
  47. package/src/system-prompt.ts +69 -26
  48. package/src/task/executor.ts +23 -16
  49. package/src/task/index.ts +7 -5
  50. package/src/task/isolation-runner.ts +15 -1
  51. package/src/task/types.ts +6 -0
  52. package/src/task/worktree.ts +219 -38
  53. package/src/tiny/title-client.ts +19 -13
  54. package/src/tools/index.ts +3 -3
  55. package/src/tools/irc.ts +9 -3
  56. package/src/tools/read.ts +28 -28
  57. package/src/utils/file-mentions.ts +10 -1
  58. package/src/utils/git.ts +38 -0
  59. package/src/web/search/providers/duckduckgo.ts +17 -3
@@ -63,8 +63,18 @@ export interface StatusOptions {
63
63
  readonly untrackedFiles?: "all" | "no" | "normal";
64
64
  readonly z?: boolean;
65
65
  }
66
+ export interface CommitAuthor {
67
+ readonly date?: string;
68
+ readonly email: string;
69
+ readonly name: string;
70
+ }
71
+ export interface CommitDetails {
72
+ readonly author: CommitAuthor;
73
+ readonly message: string;
74
+ }
66
75
  export interface CommitOptions {
67
76
  readonly allowEmpty?: boolean;
77
+ readonly author?: CommitAuthor;
68
78
  readonly files?: readonly string[];
69
79
  readonly signal?: AbortSignal;
70
80
  }
@@ -78,6 +88,7 @@ export interface PatchOptions {
78
88
  readonly cached?: boolean;
79
89
  readonly check?: boolean;
80
90
  readonly env?: Record<string, string | undefined>;
91
+ readonly threeWay?: boolean;
81
92
  readonly signal?: AbortSignal;
82
93
  }
83
94
  export interface RestoreOptions {
@@ -195,12 +206,18 @@ export declare const show: ((cwd: string, revision: string, options?: {
195
206
  /** Get the path prefix of the current directory relative to the repo root. */
196
207
  prefix(cwd: string, signal?: AbortSignal): Promise<string>;
197
208
  };
209
+ /** Read commit message and author metadata for replay/rewrite flows. */
210
+ export declare function commitDetails(cwd: string, revision: string, signal?: AbortSignal): Promise<CommitDetails>;
198
211
  export declare const log: {
199
212
  /** Recent commit subjects (one-line each). */
200
213
  subjects(cwd: string, count: number, signal?: AbortSignal): Promise<string[]>;
201
214
  /** Recent commits as `<short-sha> <subject>` onelines. */
202
215
  onelines(cwd: string, count: number, signal?: AbortSignal): Promise<string[]>;
203
216
  };
217
+ export declare const revList: {
218
+ /** Commits in `base..head`, oldest first. */
219
+ range(cwd: string, base: string, head: string, signal?: AbortSignal): Promise<string[]>;
220
+ };
204
221
  export declare const branch: {
205
222
  /** Current branch name, or null if detached/unavailable. */
206
223
  current(cwd: string, signal?: AbortSignal): Promise<string | null>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.2.6",
4
+ "version": "16.2.7",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -55,17 +55,17 @@
55
55
  "@agentclientprotocol/sdk": "0.25.0",
56
56
  "@babel/parser": "^7.29.7",
57
57
  "@mozilla/readability": "^0.6.0",
58
- "@oh-my-pi/hashline": "16.2.6",
59
- "@oh-my-pi/omp-stats": "16.2.6",
60
- "@oh-my-pi/pi-agent-core": "16.2.6",
61
- "@oh-my-pi/pi-ai": "16.2.6",
62
- "@oh-my-pi/pi-catalog": "16.2.6",
63
- "@oh-my-pi/pi-mnemopi": "16.2.6",
64
- "@oh-my-pi/pi-natives": "16.2.6",
65
- "@oh-my-pi/pi-tui": "16.2.6",
66
- "@oh-my-pi/pi-utils": "16.2.6",
67
- "@oh-my-pi/pi-wire": "16.2.6",
68
- "@oh-my-pi/snapcompact": "16.2.6",
58
+ "@oh-my-pi/hashline": "16.2.7",
59
+ "@oh-my-pi/omp-stats": "16.2.7",
60
+ "@oh-my-pi/pi-agent-core": "16.2.7",
61
+ "@oh-my-pi/pi-ai": "16.2.7",
62
+ "@oh-my-pi/pi-catalog": "16.2.7",
63
+ "@oh-my-pi/pi-mnemopi": "16.2.7",
64
+ "@oh-my-pi/pi-natives": "16.2.7",
65
+ "@oh-my-pi/pi-tui": "16.2.7",
66
+ "@oh-my-pi/pi-utils": "16.2.7",
67
+ "@oh-my-pi/pi-wire": "16.2.7",
68
+ "@oh-my-pi/snapcompact": "16.2.7",
69
69
  "@opentelemetry/api": "^1.9.1",
70
70
  "@opentelemetry/context-async-hooks": "^2.7.1",
71
71
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -10,9 +10,10 @@ import type {
10
10
  Model,
11
11
  ProviderSessionState,
12
12
  ServiceTier,
13
+ ServiceTierByFamily,
13
14
  SimpleStreamOptions,
14
15
  } from "@oh-my-pi/pi-ai";
15
- import { streamSimple } from "@oh-my-pi/pi-ai";
16
+ import { resolveModelServiceTier, streamSimple } from "@oh-my-pi/pi-ai";
16
17
  import { buildModelProviderPriorityRank, type CanonicalModelVariant } from "@oh-my-pi/pi-catalog/identity";
17
18
  import { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
18
19
  import { formatDuration, getProjectDir } from "@oh-my-pi/pi-utils";
@@ -25,7 +26,7 @@ import {
25
26
  getModelMatchPreferences,
26
27
  resolveCliModel,
27
28
  } from "../config/model-resolver";
28
- import { resolveServiceTierSetting } from "../config/service-tier";
29
+ import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
29
30
  import { Settings } from "../config/settings";
30
31
  import benchPrompt from "../prompts/bench.md" with { type: "text" };
31
32
  import { discoverAuthStorage, loadCliExtensionProviders } from "../sdk";
@@ -106,8 +107,8 @@ export interface BenchSummary {
106
107
  maxTokens: number;
107
108
  models: BenchModelReport[];
108
109
  failures: number;
109
- /** Requested service tier passed to every request; absent when none was requested. Scoped tiers (`openai-only`/`claude-only`) may be dropped per-provider downstream. */
110
- serviceTier?: ServiceTier;
110
+ /** Requested per-family service tiers, resolved per model before reaching the wire. */
111
+ serviceTierByFamily?: ServiceTierByFamily;
111
112
  }
112
113
 
113
114
  type BenchStreamSimple = (
@@ -518,12 +519,18 @@ export async function runBenchCommand(command: BenchCommandArgs, deps: BenchDepe
518
519
  const runtime = await (deps.createRuntime ?? createDefaultRuntime)();
519
520
  try {
520
521
  const targets = resolveBenchModels(command.models, runtime.modelRegistry, runtime.settings, writeStderr);
521
- // Explicit `--service-tier` wins; otherwise fall back to the configured
522
- // `serviceTier` setting (`none`/unset omits the wire field). Scope-aware
523
- // gating to the model's provider happens downstream in the provider layer.
524
- const serviceTierValue = command.flags.serviceTier ?? runtime.settings?.get("serviceTier");
525
- const serviceTier = serviceTierValue ? resolveServiceTierSetting(serviceTierValue, undefined) : undefined;
526
- if (!json && serviceTier) writeStdout(`${chalk.dim(`service tier: ${serviceTier}`)}\n`);
522
+ // Explicit `--service-tier` (a single value broadcast across families) wins;
523
+ // otherwise fall back to the configured per-family `tier.*` settings. Each
524
+ // model resolves its own family's tier below before reaching the wire.
525
+ const flagTier = command.flags.serviceTier ? serviceTierSettingToTier(command.flags.serviceTier) : undefined;
526
+ const serviceTierByFamily = command.flags.serviceTier
527
+ ? serviceTierForAllFamilies(flagTier)
528
+ : buildServiceTierByFamily(
529
+ runtime.settings?.get("tier.openai") ?? "none",
530
+ runtime.settings?.get("tier.anthropic") ?? "none",
531
+ runtime.settings?.get("tier.google") ?? "none",
532
+ );
533
+ if (!json && flagTier) writeStdout(`${chalk.dim(`service tier: ${flagTier}`)}\n`);
527
534
  const reports: BenchModelReport[] = [];
528
535
  for (const { selector, model, thinking } of targets) {
529
536
  if (!json) {
@@ -564,7 +571,7 @@ export async function runBenchCommand(command: BenchCommandArgs, deps: BenchDepe
564
571
  maxTokens,
565
572
  reasoning: toReasoningEffort(thinking),
566
573
  disableReasoning: shouldDisableReasoning(thinking) ? true : undefined,
567
- serviceTier,
574
+ serviceTier: resolveModelServiceTier(serviceTierByFamily, model),
568
575
  },
569
576
  streamFn,
570
577
  now,
@@ -606,7 +613,7 @@ export async function runBenchCommand(command: BenchCommandArgs, deps: BenchDepe
606
613
  reports.push(buildModelReport(selector, model, thinking, results));
607
614
  }
608
615
  const failures = reports.reduce((sum, report) => sum + report.results.filter(result => !result.ok).length, 0);
609
- const summary: BenchSummary = { runs, maxTokens, models: reports, failures, serviceTier };
616
+ const summary: BenchSummary = { runs, maxTokens, models: reports, failures, serviceTierByFamily };
610
617
  if (json) {
611
618
  writeStdout(`${JSON.stringify(summary, null, 2)}\n`);
612
619
  } else if (reports.length > 1 || runs > 1) {
@@ -28,12 +28,21 @@ interface ProgressReporter {
28
28
  interface DownloadResult {
29
29
  model: TinyLocalModelKey;
30
30
  ok: boolean;
31
+ error?: string;
31
32
  }
32
33
 
33
34
  function writeLine(text = ""): void {
34
35
  process.stdout.write(`${text}\n`);
35
36
  }
36
37
 
38
+ function downloadErrorSummary(error: string | undefined): string | undefined {
39
+ return error
40
+ ?.split(/\r?\n/)
41
+ .map(line => line.trim())
42
+ .find(line => line.length > 0)
43
+ ?.replace(/^Error:\s*/, "");
44
+ }
45
+
37
46
  export function resolveModels(model: string | undefined): TinyLocalModelKey[] {
38
47
  if (!model) return [DEFAULT_TINY_TITLE_LOCAL_MODEL_KEY];
39
48
  // `all` is a prefetch convenience: skip models that fail before load (unsupported
@@ -101,10 +110,15 @@ async function downloadOne(modelKey: TinyLocalModelKey, json: boolean | undefine
101
110
  const label = getTinyLocalModelSpec(modelKey)?.label ?? modelKey;
102
111
  if (!json && !process.stdout.isTTY) writeLine(`Downloading ${label} (${modelKey})...`);
103
112
  const progress = makeProgressReporter(modelKey, json);
104
- const ok = await tinyTitleClient.downloadModel(modelKey, { onProgress: progress.onProgress });
105
- progress.finish(ok);
106
- if (!json && !process.stdout.isTTY) writeLine(ok ? `Downloaded ${label}.` : `Failed to download ${label}.`);
107
- return { model: modelKey, ok };
113
+ const result = await tinyTitleClient.downloadModel(modelKey, { onProgress: progress.onProgress });
114
+ progress.finish(result.ok);
115
+ const error = downloadErrorSummary(result.error);
116
+ if (!json && !process.stdout.isTTY) {
117
+ writeLine(result.ok ? `Downloaded ${label}.` : `Failed to download ${label}${error ? `: ${error}` : ""}.`);
118
+ } else if (!json && !result.ok && error) {
119
+ writeLine(`${label} failed: ${error}`);
120
+ }
121
+ return result.error ? { model: modelKey, ok: result.ok, error: result.error } : { model: modelKey, ok: result.ok };
108
122
  }
109
123
 
110
124
  export async function runTinyModelsCommand(command: TinyModelsCommandArgs): Promise<void> {
@@ -1,6 +1,6 @@
1
1
  import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
2
2
  import { runBenchCommand } from "../cli/bench-cli";
3
- import { SERVICE_TIER_SETTING_VALUES } from "../config/service-tier";
3
+ import { SERVICE_TIER_OPENAI_VALUES } from "../config/service-tier";
4
4
 
5
5
  export default class Bench extends Command {
6
6
  static description =
@@ -19,8 +19,8 @@ export default class Bench extends Command {
19
19
  "max-tokens": Flags.integer({ description: "Max output tokens per request", default: 512 }),
20
20
  prompt: Flags.string({ description: "Custom prompt text (default: bundled bench prompt)" }),
21
21
  "service-tier": Flags.string({
22
- description: "Service tier hint (default: configured `serviceTier` setting; `none` omits it)",
23
- options: SERVICE_TIER_SETTING_VALUES,
22
+ description: "Service tier applied per model family (default: configured `tier.*` settings; `none` omits it)",
23
+ options: SERVICE_TIER_OPENAI_VALUES,
24
24
  }),
25
25
  json: Flags.boolean({ description: "Output JSON" }),
26
26
  par: Flags.integer({ description: "Execute runs with N parallel queries/requests", default: 4 }),
@@ -22,7 +22,16 @@
22
22
  },
23
23
  "disabledServers": {
24
24
  "type": "array",
25
- "description": "User-level denylist for disabling discovered servers by name.",
25
+ "description": "User-level denylist for disabling discovered servers by name. Highest precedence: a server here is hidden regardless of any other source.",
26
+ "items": {
27
+ "type": "string",
28
+ "minLength": 1
29
+ },
30
+ "uniqueItems": true
31
+ },
32
+ "enabledServers": {
33
+ "type": "array",
34
+ "description": "User-level allowlist that overrides a discovered server's `enabled: false` flag (e.g. when the source config is owned by another tool such as opencode.json). The denylist still wins.",
26
35
  "items": {
27
36
  "type": "string",
28
37
  "minLength": 1
@@ -1,87 +1,116 @@
1
- import type { ServiceTier } from "@oh-my-pi/pi-ai";
1
+ import type { ServiceTier, ServiceTierByFamily } from "@oh-my-pi/pi-ai";
2
2
  import type { SubmenuOption } from "./settings-schema";
3
3
 
4
4
  /**
5
- * Service-tier setting values shared by every "Service Tier" setting. `"none"`
6
- * is the omit-the-parameter sentinel; the remaining values mirror
7
- * {@link ServiceTier}.
5
+ * Per-family service-tier setting values. `"none"` is the omit-the-parameter
6
+ * sentinel; the rest mirror the wire {@link ServiceTier} values each provider
7
+ * family actually realizes. OpenAI accepts the full set; Anthropic realizes
8
+ * only `priority` (fast mode); Google (Gemini API + Vertex) realizes
9
+ * `flex`/`priority`.
8
10
  */
9
- export const SERVICE_TIER_SETTING_VALUES = [
11
+ export const SERVICE_TIER_OPENAI_VALUES = ["none", "auto", "default", "flex", "scale", "priority"] as const;
12
+ export const SERVICE_TIER_ANTHROPIC_VALUES = ["none", "priority"] as const;
13
+ export const SERVICE_TIER_GOOGLE_VALUES = ["none", "flex", "priority"] as const;
14
+
15
+ export type ServiceTierOpenAISettingValue = (typeof SERVICE_TIER_OPENAI_VALUES)[number];
16
+ export type ServiceTierAnthropicSettingValue = (typeof SERVICE_TIER_ANTHROPIC_VALUES)[number];
17
+ export type ServiceTierGoogleSettingValue = (typeof SERVICE_TIER_GOOGLE_VALUES)[number];
18
+
19
+ /**
20
+ * Inherit-capable single value for the subagent/advisor tiers. The chosen tier
21
+ * is broadcast across families and applied to whichever family the spawned
22
+ * model belongs to (clamped to what that family realizes); `"inherit"` defers
23
+ * to the main agent's live per-family selection.
24
+ */
25
+ export const SERVICE_TIER_INHERIT_SETTING_VALUES = [
26
+ "inherit",
10
27
  "none",
11
28
  "auto",
12
29
  "default",
13
30
  "flex",
14
31
  "scale",
15
32
  "priority",
16
- "openai-only",
17
- "claude-only",
18
33
  ] as const;
19
34
 
20
- export type ServiceTierSettingValue = (typeof SERVICE_TIER_SETTING_VALUES)[number];
21
-
22
- /** Variant value set for scoped service-tier settings (subagent/advisor) that can defer to the main agent. */
23
- export const SERVICE_TIER_INHERIT_SETTING_VALUES = ["inherit", ...SERVICE_TIER_SETTING_VALUES] as const;
24
-
25
35
  export type ServiceTierInheritSettingValue = (typeof SERVICE_TIER_INHERIT_SETTING_VALUES)[number];
26
36
 
27
- /** Submenu descriptions shared by the base `serviceTier` setting. */
28
- export const SERVICE_TIER_OPTIONS: ReadonlyArray<SubmenuOption<ServiceTierSettingValue>> = [
29
- { value: "none", label: "None", description: "Omit service_tier parameter" },
30
- { value: "auto", label: "Auto", description: "Use provider default tier selection (OpenAI)" },
31
- { value: "default", label: "Default", description: "Standard priority processing (OpenAI)" },
32
- { value: "flex", label: "Flex", description: "Flexible capacity tier when available (OpenAI)" },
33
- { value: "scale", label: "Scale", description: "Scale Tier credits when available (OpenAI)" },
37
+ export const SERVICE_TIER_OPENAI_OPTIONS: ReadonlyArray<SubmenuOption<ServiceTierOpenAISettingValue>> = [
38
+ { value: "none", label: "None", description: "Omit service_tier (standard processing)" },
39
+ { value: "auto", label: "Auto", description: "Provider default tier selection" },
40
+ { value: "default", label: "Default", description: "Standard priority processing" },
41
+ { value: "flex", label: "Flex", description: "Lower cost, higher latency when available" },
42
+ { value: "scale", label: "Scale", description: "Scale Tier credits when available" },
43
+ { value: "priority", label: "Priority", description: "Faster, higher cost (premium request)" },
44
+ ];
45
+
46
+ export const SERVICE_TIER_ANTHROPIC_OPTIONS: ReadonlyArray<SubmenuOption<ServiceTierAnthropicSettingValue>> = [
47
+ { value: "none", label: "None", description: "Standard processing" },
34
48
  {
35
49
  value: "priority",
36
50
  label: "Priority",
37
- description: "Priority on every supported provider (OpenAI `service_tier`, Anthropic fast mode)",
38
- },
39
- {
40
- value: "openai-only",
41
- label: "Priority (OpenAI only)",
42
- description: "Priority on OpenAI/OpenAI-Codex requests; ignored elsewhere",
43
- },
44
- {
45
- value: "claude-only",
46
- label: "Priority (Claude only)",
47
- description: "Anthropic fast mode on direct Claude requests; ignored elsewhere (incl. Bedrock/Vertex)",
51
+ description: 'Fast mode (`speed: "fast"`) on supported direct Claude models; ignored on Bedrock/Vertex',
48
52
  },
49
53
  ];
50
54
 
51
- /** Submenu descriptions for inherit-capable service-tier settings. */
55
+ export const SERVICE_TIER_GOOGLE_OPTIONS: ReadonlyArray<SubmenuOption<ServiceTierGoogleSettingValue>> = [
56
+ { value: "none", label: "None", description: "Standard processing" },
57
+ { value: "flex", label: "Flex", description: "Lower cost, higher latency (Gemini API + Vertex)" },
58
+ { value: "priority", label: "Priority", description: "Faster, higher reliability (Gemini API + Vertex)" },
59
+ ];
60
+
52
61
  export const SERVICE_TIER_INHERIT_OPTIONS: ReadonlyArray<SubmenuOption<ServiceTierInheritSettingValue>> = [
53
- { value: "inherit", label: "Inherit", description: "Use the main agent's Service Tier" },
54
- ...SERVICE_TIER_OPTIONS,
62
+ { value: "inherit", label: "Inherit", description: "Match the main agent's live per-family tiers" },
63
+ { value: "none", label: "None", description: "Standard processing" },
64
+ { value: "auto", label: "Auto", description: "Provider default tier selection (OpenAI family)" },
65
+ { value: "default", label: "Default", description: "Standard priority processing (OpenAI family)" },
66
+ { value: "flex", label: "Flex", description: "Flexible capacity tier (OpenAI/Google families)" },
67
+ { value: "scale", label: "Scale", description: "Scale Tier credits (OpenAI family)" },
68
+ { value: "priority", label: "Priority", description: "Priority on every supported family of the spawned model" },
55
69
  ];
56
70
 
71
+ /** Map a per-family setting value to a wire {@link ServiceTier}, or `undefined` to omit. */
72
+ export function serviceTierSettingToTier(value: string): ServiceTier | undefined {
73
+ if (value === "none" || value === "" || value === "inherit") return undefined;
74
+ return value as ServiceTier;
75
+ }
76
+
77
+ /** Assemble the live per-family tier map from the three `tier.*` setting values. */
78
+ export function buildServiceTierByFamily(openai: string, anthropic: string, google: string): ServiceTierByFamily {
79
+ const out: ServiceTierByFamily = {};
80
+ const o = serviceTierSettingToTier(openai);
81
+ if (o) out.openai = o;
82
+ const a = serviceTierSettingToTier(anthropic);
83
+ if (a) out.anthropic = a;
84
+ const g = serviceTierSettingToTier(google);
85
+ if (g) out.google = g;
86
+ return out;
87
+ }
88
+
57
89
  /**
58
- * Resolve a service-tier setting value to the wire {@link ServiceTier} (or
59
- * `undefined` to omit). `"inherit"` defers to `inherited`; `"none"` omits.
90
+ * Broadcast a single chosen tier across families, clamped to what each family
91
+ * realizes: OpenAI takes any tier, Anthropic only `priority`, Google only
92
+ * `flex`/`priority`. Used by the subagent/advisor single-value settings and the
93
+ * `omp bench --service-tier` flag, which apply one tier to whatever family the
94
+ * target model belongs to.
60
95
  */
61
- export function resolveServiceTierSetting(value: string, inherited: ServiceTier | undefined): ServiceTier | undefined {
62
- if (value === "inherit") return inherited;
63
- if (value === "none" || value === "") return undefined;
64
- return value as ServiceTier;
96
+ export function serviceTierForAllFamilies(tier: ServiceTier | undefined): ServiceTierByFamily {
97
+ if (!tier) return {};
98
+ const out: ServiceTierByFamily = { openai: tier };
99
+ if (tier === "priority") out.anthropic = "priority";
100
+ if (tier === "flex" || tier === "priority") out.google = tier;
101
+ return out;
65
102
  }
66
103
 
67
104
  /**
68
- * Resolve the `serviceTier` *setting value* to stamp onto a subagent's settings
69
- * snapshot.
105
+ * Resolve a subagent/advisor service-tier setting to a per-family map.
70
106
  *
71
- * - A concrete `subagentSetting` (`"none"` or a tier) wins outright.
72
- * - `"inherit"` defers to the parent's live effective tier when the caller has a
73
- * live session (`inherited` passed as `ServiceTier | null`, where `null` means
74
- * the parent explicitly has no tiere.g. `/fast off`). When no live session
75
- * is available (`inherited === undefined`, e.g. cold subagent revive) it falls
76
- * back to the parent's configured `serviceTier` setting so behavior matches a
77
- * plain settings snapshot.
107
+ * - A concrete tier is broadcast across families (see
108
+ * {@link serviceTierForAllFamilies}).
109
+ * - `"none"` yields an empty map.
110
+ * - `"inherit"` defers to `inherited`the parent's live per-family tiers when
111
+ * a live session supplied them, else the empty map.
78
112
  */
79
- export function resolveSubagentServiceTier(
80
- subagentSetting: string,
81
- configuredTier: ServiceTierSettingValue,
82
- inherited: ServiceTier | null | undefined,
83
- ): ServiceTierSettingValue {
84
- if (subagentSetting !== "inherit") return subagentSetting as ServiceTierSettingValue;
85
- if (inherited === undefined) return configuredTier;
86
- return inherited ?? "none";
113
+ export function resolveSubagentServiceTier(setting: string, inherited: ServiceTierByFamily): ServiceTierByFamily {
114
+ if (setting === "inherit") return inherited;
115
+ return serviceTierForAllFamilies(serviceTierSettingToTier(setting));
87
116
  }
@@ -36,10 +36,14 @@ import {
36
36
  import { EDIT_MODES } from "../utils/edit-mode";
37
37
  import { SEARCH_PROVIDER_OPTIONS, SEARCH_PROVIDER_PREFERENCES, type SearchProviderId } from "../web/search/types";
38
38
  import {
39
+ SERVICE_TIER_ANTHROPIC_OPTIONS,
40
+ SERVICE_TIER_ANTHROPIC_VALUES,
41
+ SERVICE_TIER_GOOGLE_OPTIONS,
42
+ SERVICE_TIER_GOOGLE_VALUES,
39
43
  SERVICE_TIER_INHERIT_OPTIONS,
40
44
  SERVICE_TIER_INHERIT_SETTING_VALUES,
41
- SERVICE_TIER_OPTIONS,
42
- SERVICE_TIER_SETTING_VALUES,
45
+ SERVICE_TIER_OPENAI_OPTIONS,
46
+ SERVICE_TIER_OPENAI_VALUES,
43
47
  } from "./service-tier";
44
48
 
45
49
  /** Unified settings schema - single source of truth for all settings.
@@ -1214,72 +1218,74 @@ export const SETTINGS_SCHEMA = {
1214
1218
  },
1215
1219
  },
1216
1220
 
1217
- serviceTier: {
1221
+ "tier.openai": {
1218
1222
  type: "enum",
1219
- values: SERVICE_TIER_SETTING_VALUES,
1223
+ values: SERVICE_TIER_OPENAI_VALUES,
1220
1224
  default: "none",
1221
1225
  ui: {
1222
1226
  tab: "model",
1223
1227
  group: "Sampling",
1224
- label: "Service Tier",
1228
+ label: "Service Tier — OpenAI",
1225
1229
  description:
1226
- 'Processing priority hint (none = omit). OpenAI accepts the tier values directly; Anthropic realizes `priority` as `speed: "fast"` on supported Opus models. Scoped values target one family.',
1227
- options: SERVICE_TIER_OPTIONS,
1230
+ "Processing tier for OpenAI / OpenAI-Codex requests, and OpenAI-family models routed via OpenRouter (none = omit). Sent as `service_tier`.",
1231
+ options: SERVICE_TIER_OPENAI_OPTIONS,
1228
1232
  },
1229
1233
  },
1230
1234
 
1231
- serviceTierSubagent: {
1235
+ "tier.anthropic": {
1232
1236
  type: "enum",
1233
- values: SERVICE_TIER_INHERIT_SETTING_VALUES,
1234
- default: "inherit",
1237
+ values: SERVICE_TIER_ANTHROPIC_VALUES,
1238
+ default: "none",
1235
1239
  ui: {
1236
1240
  tab: "model",
1237
1241
  group: "Sampling",
1238
- label: "Service Tier - Subagent",
1242
+ label: "Service Tier Anthropic",
1239
1243
  description:
1240
- "Service Tier for spawned task/eval subagents. Inherit = match the main agent's live tier (tracks /fast); pick a value to scope subagents independently.",
1241
- options: SERVICE_TIER_INHERIT_OPTIONS,
1244
+ 'Processing tier for Claude requests. `priority` realizes fast mode (`speed: "fast"`) on supported direct Anthropic models; ignored on Bedrock/Vertex Claude and via OpenRouter.',
1245
+ options: SERVICE_TIER_ANTHROPIC_OPTIONS,
1242
1246
  },
1243
1247
  },
1244
1248
 
1245
- serviceTierAdvisor: {
1249
+ "tier.google": {
1246
1250
  type: "enum",
1247
- values: SERVICE_TIER_INHERIT_SETTING_VALUES,
1251
+ values: SERVICE_TIER_GOOGLE_VALUES,
1248
1252
  default: "none",
1249
1253
  ui: {
1250
1254
  tab: "model",
1251
1255
  group: "Sampling",
1252
- label: "Service Tier - Advisor",
1256
+ label: "Service Tier Google",
1257
+ description:
1258
+ "Processing tier for Gemini (Google AI Studio + Vertex) requests, and Google-family models routed via OpenRouter (none = omit). Sent as the top-level `serviceTier` field.",
1259
+ options: SERVICE_TIER_GOOGLE_OPTIONS,
1260
+ },
1261
+ },
1262
+
1263
+ "tier.subagent": {
1264
+ type: "enum",
1265
+ values: SERVICE_TIER_INHERIT_SETTING_VALUES,
1266
+ default: "inherit",
1267
+ ui: {
1268
+ tab: "model",
1269
+ group: "Sampling",
1270
+ label: "Service Tier — Subagent",
1253
1271
  description:
1254
- "Service Tier for the advisor model. None = standard processing; Inherit = match the main agent's live tier; pick a value (e.g. Priority) to run the advisor on a faster serving path.",
1272
+ "Service Tier for spawned task/eval subagents. Inherit = match the main agent's live per-family tiers (tracks /fast); pick a value to apply it to whichever family the subagent's model belongs to.",
1255
1273
  options: SERVICE_TIER_INHERIT_OPTIONS,
1256
- condition: "advisorEnabled",
1257
1274
  },
1258
1275
  },
1259
1276
 
1260
- fastModeScope: {
1277
+ "tier.advisor": {
1261
1278
  type: "enum",
1262
- values: ["both", "openai", "claude"] as const,
1263
- default: "both",
1279
+ values: SERVICE_TIER_INHERIT_SETTING_VALUES,
1280
+ default: "none",
1264
1281
  ui: {
1265
1282
  tab: "model",
1266
1283
  group: "Sampling",
1267
- label: "Fast Mode Scope",
1284
+ label: "Service Tier — Advisor",
1268
1285
  description:
1269
- 'Which providers `/fast on` (and the fast-mode toggle) target. "both" = priority on every supported provider; "openai"/"claude" scope it to one family (mirrors serviceTier openai-only/claude-only).',
1270
- options: [
1271
- { value: "both", label: "Both", description: "Priority on every supported provider" },
1272
- {
1273
- value: "openai",
1274
- label: "OpenAI only",
1275
- description: "Priority on OpenAI/OpenAI-Codex requests; ignored elsewhere",
1276
- },
1277
- {
1278
- value: "claude",
1279
- label: "Claude only",
1280
- description: "Anthropic fast mode on direct Claude requests; ignored elsewhere",
1281
- },
1282
- ],
1286
+ "Service Tier for the advisor model. None = standard processing; Inherit = match the main agent's live per-family tiers; pick a value to apply it to the advisor model's family.",
1287
+ options: SERVICE_TIER_INHERIT_OPTIONS,
1288
+ condition: "advisorEnabled",
1283
1289
  },
1284
1290
  },
1285
1291
 
@@ -1148,6 +1148,53 @@ export class Settings {
1148
1148
  // the incoherent "hashline edits without addressable anchors" state.
1149
1149
  delete raw.readHashLines;
1150
1150
 
1151
+ // serviceTier (single enum with scoped openai-only/claude-only sentinels)
1152
+ // → per-family tier.openai/tier.anthropic/tier.google; serviceTierSubagent
1153
+ // → tier.subagent; serviceTierAdvisor → tier.advisor. `fastModeScope` is
1154
+ // dropped — per-family scoping is now expressed by the three tier settings.
1155
+ const tierObj = isRecord(raw.tier) ? raw.tier : {};
1156
+ let tierTouched = false;
1157
+ const setTier = (family: string, value: unknown): void => {
1158
+ if (value !== undefined && !(family in tierObj)) {
1159
+ tierObj[family] = value;
1160
+ tierTouched = true;
1161
+ }
1162
+ };
1163
+ if (typeof raw.serviceTier === "string") {
1164
+ switch (raw.serviceTier) {
1165
+ case "priority":
1166
+ setTier("openai", "priority");
1167
+ setTier("anthropic", "priority");
1168
+ setTier("google", "priority");
1169
+ break;
1170
+ case "openai-only":
1171
+ setTier("openai", "priority");
1172
+ break;
1173
+ case "claude-only":
1174
+ setTier("anthropic", "priority");
1175
+ break;
1176
+ case "auto":
1177
+ case "default":
1178
+ case "flex":
1179
+ case "scale":
1180
+ setTier("openai", raw.serviceTier);
1181
+ break;
1182
+ }
1183
+ delete raw.serviceTier;
1184
+ }
1185
+ const mapInheritTier = (value: unknown): unknown =>
1186
+ value === "openai-only" || value === "claude-only" ? "priority" : value;
1187
+ if ("serviceTierSubagent" in raw) {
1188
+ setTier("subagent", mapInheritTier(raw.serviceTierSubagent));
1189
+ delete raw.serviceTierSubagent;
1190
+ }
1191
+ if ("serviceTierAdvisor" in raw) {
1192
+ setTier("advisor", mapInheritTier(raw.serviceTierAdvisor));
1193
+ delete raw.serviceTierAdvisor;
1194
+ }
1195
+ if (tierTouched) raw.tier = tierObj;
1196
+ delete raw.fastModeScope;
1197
+
1151
1198
  return raw;
1152
1199
  }
1153
1200
 
@@ -405,8 +405,10 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
405
405
  parentMnemopiSessionState: options.session.getMnemopiSessionState?.(),
406
406
  parentTelemetry: options.session.getTelemetry?.(),
407
407
  parentAgentId: options.session.getAgentId?.() ?? MAIN_AGENT_ID,
408
- // Live source of truth for `serviceTierSubagent: inherit` (null = explicit none).
409
- parentServiceTier: options.session.getServiceTier ? (options.session.getServiceTier() ?? null) : undefined,
408
+ // Live source of truth for `tier.subagent: inherit` (null = explicit none).
409
+ parentServiceTier: options.session.getServiceTierByFamily
410
+ ? (options.session.getServiceTierByFamily() ?? null)
411
+ : undefined,
410
412
  // Deliberately omit parentEvalSessionId: the parent's Python kernel is
411
413
  // blocked on this bridge call, so sharing the eval session would deadlock
412
414
  // (subagent queues behind the parent's in-flight execution, parent waits