1import * as Effect from "effect/Effect";
2import * as Schema from "effect/Schema";
3import * as SchemaTransformation from "effect/SchemaTransformation";
4import { TrimmedNonEmptyString } from "./baseSchemas.ts";
5import { ProviderDriverKind } from "./providerInstance.ts";
6
7export const ProviderOptionDescriptorType = Schema.Literals(["select", "boolean"]);
8export type ProviderOptionDescriptorType = typeof ProviderOptionDescriptorType.Type;
9
10export const ProviderOptionChoice = Schema.Struct({
11 id: TrimmedNonEmptyString,
12 label: TrimmedNonEmptyString,
13 description: Schema.optional(TrimmedNonEmptyString),
14 isDefault: Schema.optional(Schema.Boolean),
15});
16export type ProviderOptionChoice = typeof ProviderOptionChoice.Type;
17
18const ProviderOptionDescriptorBase = {
19 id: TrimmedNonEmptyString,
20 label: TrimmedNonEmptyString,
21 description: Schema.optional(TrimmedNonEmptyString),
22} as const;
23
24export const SelectProviderOptionDescriptor = Schema.Struct({
25 ...ProviderOptionDescriptorBase,
26 type: Schema.Literal("select"),
27 options: Schema.Array(ProviderOptionChoice),
28 currentValue: Schema.optional(TrimmedNonEmptyString),
29 promptInjectedValues: Schema.optional(Schema.Array(TrimmedNonEmptyString)),
30});
31export type SelectProviderOptionDescriptor = typeof SelectProviderOptionDescriptor.Type;
32
33export const BooleanProviderOptionDescriptor = Schema.Struct({
34 ...ProviderOptionDescriptorBase,
35 type: Schema.Literal("boolean"),
36 currentValue: Schema.optional(Schema.Boolean),
37});
38export type BooleanProviderOptionDescriptor = typeof BooleanProviderOptionDescriptor.Type;
39
40export const ProviderOptionDescriptor = Schema.Union([
41 SelectProviderOptionDescriptor,
42 BooleanProviderOptionDescriptor,
43]);
44export type ProviderOptionDescriptor = typeof ProviderOptionDescriptor.Type;
45
46export const ProviderOptionSelectionValue = Schema.Union([TrimmedNonEmptyString, Schema.Boolean]);
47export type ProviderOptionSelectionValue = typeof ProviderOptionSelectionValue.Type;
48
49export const ProviderOptionSelection = Schema.Struct({
50 id: TrimmedNonEmptyString,
51 value: ProviderOptionSelectionValue,
52});
53export type ProviderOptionSelection = typeof ProviderOptionSelection.Type;
54
55/**
56 * Legacy on-disk shape for provider option selections, kept readable by the
57 * decoder so we can tolerate stored data written before the v3 array shape.
58 *
59 * Persisted historically as `{ effort: "max", fastMode: true, ... }` inside
60 * `modelSelection.options`. Migration 026 rewrites stored rows to the
61 * canonical array shape, but we still see the legacy form in:
62 * - `settings.json` files from older client builds,
63 * - SQLite databases that have not yet run migration 026,
64 * - any future regression that re-introduces the legacy shape.
65 */
66const LegacyProviderOptionSelectionsObject = Schema.Record(Schema.String, Schema.Unknown);
67
68const ProviderOptionSelectionsFromLegacyObject = LegacyProviderOptionSelectionsObject.pipe(
69 Schema.decodeTo(
70 Schema.Array(ProviderOptionSelection),
71 SchemaTransformation.transformEffect({
72 decode: (record) => Effect.succeed(coerceLegacyOptionsObjectToArray(record)),
73 encode: (selections) => Effect.succeed(canonicalSelectionsToLegacyObject(selections)),
74 }),
75 ),
76);
77
78/**
79 * Schema for the `options` field of every `ModelSelection` variant.
80 *
81 * Accepts both:
82 * - the canonical array shape `Array<{ id, value }>` (preferred), and
83 * - the legacy object shape `Record<string, string | boolean | …>` from
84 * pre-migration data.
85 *
86 * Always normalizes to the canonical array on decode and re-encodes as the
87 * canonical array, so any legacy storage gets cleaned up the next time the
88 * containing record is written back.
89 */
90export const ProviderOptionSelections = Schema.Union([
91 Schema.Array(ProviderOptionSelection),
92 ProviderOptionSelectionsFromLegacyObject,
93]);
94export type ProviderOptionSelections = typeof ProviderOptionSelections.Type;
95
96function coerceLegacyOptionsObjectToArray(
97 record: Record<string, unknown>,
98): ReadonlyArray<ProviderOptionSelection> {
99 const entries: Array<ProviderOptionSelection> = [];
100 for (const [rawKey, rawValue] of Object.entries(record)) {
101 const id = typeof rawKey === "string" ? rawKey.trim() : "";
102 if (id.length === 0) continue;
103 if (typeof rawValue === "string") {
104 const trimmed = rawValue.trim();
105 if (trimmed.length > 0) entries.push({ id, value: trimmed });
106 } else if (typeof rawValue === "boolean") {
107 entries.push({ id, value: rawValue });
108 }
109 // Drop anything else (numbers, null, nested objects/arrays) to match the
110 // permissive normalization performed by migration 026.
111 }
112 return entries;
113}
114
115function canonicalSelectionsToLegacyObject(
116 selections: ReadonlyArray<ProviderOptionSelection>,
117): Record<string, string | boolean> {
118 const out: Record<string, string | boolean> = {};
119 for (const { id, value } of selections) {
120 out[id] = value;
121 }
122 return out;
123}
124
125export const ModelCapabilities = Schema.Struct({
126 optionDescriptors: Schema.optional(Schema.Array(ProviderOptionDescriptor)),
127});
128export type ModelCapabilities = typeof ModelCapabilities.Type;
129
130/**
131 * A user-authored custom model. `name` and `capabilities` are optional so a
132 * bare slug keeps its driver-default presentation; when `capabilities` is
133 * set, its descriptors replace the driver default in the model picker.
134 */
135export const CustomModelEntry = Schema.Struct({
136 slug: TrimmedNonEmptyString,
137 name: Schema.optional(TrimmedNonEmptyString),
138 capabilities: Schema.optional(ModelCapabilities),
139});
140export type CustomModelEntry = typeof CustomModelEntry.Type;
141
142/** On-disk custom model setting: the legacy bare slug, or a full entry. */
143export const CustomModelSetting = Schema.Union([Schema.String, CustomModelEntry]);
144export type CustomModelSetting = typeof CustomModelSetting.Type;
145
146const CODEX_DRIVER_KIND = ProviderDriverKind.make("codex");
147const CLAUDE_DRIVER_KIND = ProviderDriverKind.make("claudeAgent");
148const CURSOR_DRIVER_KIND = ProviderDriverKind.make("cursor");
149const GROK_DRIVER_KIND = ProviderDriverKind.make("grok");
150const MUSE_DRIVER_KIND = ProviderDriverKind.make("muse");
151const PI_DRIVER_KIND = ProviderDriverKind.make("pi");
152const ACP_REGISTRY_DRIVER_KIND = ProviderDriverKind.make("acpRegistry");
153const OPENCODE_DRIVER_KIND = ProviderDriverKind.make("opencode");
154
155export const DEFAULT_MODEL = "gpt-6-astra";
156
157/**
158 * Codex default-model preference, most preferred first. The provider snapshot
159 * marks the first of these present in the live `model/list` response as
160 * default; when none are available, Codex's own `isDefault` flag wins.
161 */
162export const PREFERRED_DEFAULT_CODEX_MODELS: ReadonlyArray<string> = [
163 DEFAULT_MODEL,
164 "gpt-5.6-sol",
165 "gpt-5.6-terra",
166];
167export const DEFAULT_TEXT_GENERATION_MODEL = "gpt-6-luna";
168/** Keep the official Antigravity session's current model. Never send this ID to ACP. */
169export const ANTIGRAVITY_DEFAULT_MODEL = "antigravity-default";
170export const DEFAULT_TEXT_GENERATION_REASONING_EFFORT = "low";
171
172/** Let Muse pick the account's default model. Never send this ID to Muse. */
173export const MUSE_DEFAULT_MODEL = "default";
174
175export const DEFAULT_MODEL_BY_PROVIDER: Partial<Record<ProviderDriverKind, string>> = {
176 [CODEX_DRIVER_KIND]: DEFAULT_MODEL,
177 [CLAUDE_DRIVER_KIND]: "claude-fable-5-1",
178 [CURSOR_DRIVER_KIND]: "auto",
179 // Product slug, not an ACP model id. The Grok adapter treats it as "the session's current model".
180 [GROK_DRIVER_KIND]: "grok-build",
181 [MUSE_DRIVER_KIND]: MUSE_DEFAULT_MODEL,
182 [ACP_REGISTRY_DRIVER_KIND]: "default",
183 // "default" defers to the user's own Pi settings.json model selection.
184 [PI_DRIVER_KIND]: "default",
185 [OPENCODE_DRIVER_KIND]: "openai/gpt-5",
186 [ProviderDriverKind.make("antigravity")]: ANTIGRAVITY_DEFAULT_MODEL,
187};
188
189/** Per-provider text generation model defaults. */
190export const DEFAULT_TEXT_GENERATION_MODEL_BY_PROVIDER: Partial<
191 Record<ProviderDriverKind, string>
192> = {
193 [CODEX_DRIVER_KIND]: DEFAULT_TEXT_GENERATION_MODEL,
194 [ProviderDriverKind.make("antigravity")]: ANTIGRAVITY_DEFAULT_MODEL,
195 [CLAUDE_DRIVER_KIND]: "claude-haiku-5-5",
196 [CURSOR_DRIVER_KIND]: "composer-2",
197 [OPENCODE_DRIVER_KIND]: "openai/gpt-5",
198};
199
200export const MODEL_SLUG_ALIASES_BY_PROVIDER: Partial<
201 Record<ProviderDriverKind, Record<string, string>>
202> = {
203 [CODEX_DRIVER_KIND]: {
204 "gpt-5-codex": "gpt-5.4",
205 "5.4": "gpt-5.4",
206 "5.3": "gpt-5.3-codex",
207 "gpt-5.3": "gpt-5.3-codex",
208 "5.3-spark": "gpt-5.3-codex-spark",
209 "gpt-5.3-spark": "gpt-5.3-codex-spark",
210 },
211 [CLAUDE_DRIVER_KIND]: {},
212 [CURSOR_DRIVER_KIND]: {
213 composer: "composer-2",
214 "composer-1.5": "composer-1.5",
215 "composer-1": "composer-1.5",
216 "opus-4.6-thinking": "claude-opus-4-6",
217 "opus-4.6": "claude-opus-4-6",
218 "sonnet-4.6-thinking": "claude-sonnet-4-6",
219 "sonnet-4.6": "claude-sonnet-4-6",
220 "opus-4.5-thinking": "claude-opus-4-5",
221 "opus-4.5": "claude-opus-4-5",
222 },
223 [OPENCODE_DRIVER_KIND]: {},
224};
225
226// ── Provider display names ────────────────────────────────────────────
227
228export const PROVIDER_DISPLAY_NAMES: Partial<Record<ProviderDriverKind, string>> = {
229 [ProviderDriverKind.make("antigravity")]: "Antigravity",
230 [CODEX_DRIVER_KIND]: "Codex",
231 [CLAUDE_DRIVER_KIND]: "Claude",
232 [CURSOR_DRIVER_KIND]: "Cursor",
233 [GROK_DRIVER_KIND]: "Grok",
234 [MUSE_DRIVER_KIND]: "Muse Code",
235 [ACP_REGISTRY_DRIVER_KIND]: "ACP Registry",
236 [PI_DRIVER_KIND]: "Pi",
237 [OPENCODE_DRIVER_KIND]: "OpenCode",
238};