| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Schema from "effect/Schema"; |
| 3 | import * as SchemaTransformation from "effect/SchemaTransformation"; |
| 4 | import { TrimmedNonEmptyString } from "./baseSchemas.ts"; |
| 5 | import { ProviderDriverKind } from "./providerInstance.ts"; |
| 6 | |
| 7 | export const ProviderOptionDescriptorType = Schema.Literals(["select", "boolean"]); |
| 8 | export type ProviderOptionDescriptorType = typeof ProviderOptionDescriptorType.Type; |
| 9 | |
| 10 | export const ProviderOptionChoice = Schema.Struct({ |
| 11 | id: TrimmedNonEmptyString, |
| 12 | label: TrimmedNonEmptyString, |
| 13 | description: Schema.optional(TrimmedNonEmptyString), |
| 14 | isDefault: Schema.optional(Schema.Boolean), |
| 15 | }); |
| 16 | export type ProviderOptionChoice = typeof ProviderOptionChoice.Type; |
| 17 | |
| 18 | const ProviderOptionDescriptorBase = { |
| 19 | id: TrimmedNonEmptyString, |
| 20 | label: TrimmedNonEmptyString, |
| 21 | description: Schema.optional(TrimmedNonEmptyString), |
| 22 | } as const; |
| 23 | |
| 24 | export 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 | }); |
| 31 | export type SelectProviderOptionDescriptor = typeof SelectProviderOptionDescriptor.Type; |
| 32 | |
| 33 | export const BooleanProviderOptionDescriptor = Schema.Struct({ |
| 34 | ...ProviderOptionDescriptorBase, |
| 35 | type: Schema.Literal("boolean"), |
| 36 | currentValue: Schema.optional(Schema.Boolean), |
| 37 | }); |
| 38 | export type BooleanProviderOptionDescriptor = typeof BooleanProviderOptionDescriptor.Type; |
| 39 | |
| 40 | export const ProviderOptionDescriptor = Schema.Union([ |
| 41 | SelectProviderOptionDescriptor, |
| 42 | BooleanProviderOptionDescriptor, |
| 43 | ]); |
| 44 | export type ProviderOptionDescriptor = typeof ProviderOptionDescriptor.Type; |
| 45 | |
| 46 | export const ProviderOptionSelectionValue = Schema.Union([TrimmedNonEmptyString, Schema.Boolean]); |
| 47 | export type ProviderOptionSelectionValue = typeof ProviderOptionSelectionValue.Type; |
| 48 | |
| 49 | export const ProviderOptionSelection = Schema.Struct({ |
| 50 | id: TrimmedNonEmptyString, |
| 51 | value: ProviderOptionSelectionValue, |
| 52 | }); |
| 53 | export 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 | */ |
| 66 | const LegacyProviderOptionSelectionsObject = Schema.Record(Schema.String, Schema.Unknown); |
| 67 | |
| 68 | const 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 | */ |
| 90 | export const ProviderOptionSelections = Schema.Union([ |
| 91 | Schema.Array(ProviderOptionSelection), |
| 92 | ProviderOptionSelectionsFromLegacyObject, |
| 93 | ]); |
| 94 | export type ProviderOptionSelections = typeof ProviderOptionSelections.Type; |
| 95 | |
| 96 | function 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 | |
| 115 | function 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 | |
| 125 | export const ModelCapabilities = Schema.Struct({ |
| 126 | optionDescriptors: Schema.optional(Schema.Array(ProviderOptionDescriptor)), |
| 127 | }); |
| 128 | export 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 | */ |
| 135 | export const CustomModelEntry = Schema.Struct({ |
| 136 | slug: TrimmedNonEmptyString, |
| 137 | name: Schema.optional(TrimmedNonEmptyString), |
| 138 | capabilities: Schema.optional(ModelCapabilities), |
| 139 | }); |
| 140 | export type CustomModelEntry = typeof CustomModelEntry.Type; |
| 141 | |
| 142 | /** On-disk custom model setting: the legacy bare slug, or a full entry. */ |
| 143 | export const CustomModelSetting = Schema.Union([Schema.String, CustomModelEntry]); |
| 144 | export type CustomModelSetting = typeof CustomModelSetting.Type; |
| 145 | |
| 146 | const CODEX_DRIVER_KIND = ProviderDriverKind.make("codex"); |
| 147 | const CLAUDE_DRIVER_KIND = ProviderDriverKind.make("claudeAgent"); |
| 148 | const CURSOR_DRIVER_KIND = ProviderDriverKind.make("cursor"); |
| 149 | const GROK_DRIVER_KIND = ProviderDriverKind.make("grok"); |
| 150 | const MUSE_DRIVER_KIND = ProviderDriverKind.make("muse"); |
| 151 | const PI_DRIVER_KIND = ProviderDriverKind.make("pi"); |
| 152 | const ACP_REGISTRY_DRIVER_KIND = ProviderDriverKind.make("acpRegistry"); |
| 153 | const OPENCODE_DRIVER_KIND = ProviderDriverKind.make("opencode"); |
| 154 | |
| 155 | export 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 | */ |
| 162 | export const PREFERRED_DEFAULT_CODEX_MODELS: ReadonlyArray<string> = [ |
| 163 | DEFAULT_MODEL, |
| 164 | "gpt-5.6-sol", |
| 165 | "gpt-5.6-terra", |
| 166 | ]; |
| 167 | export const DEFAULT_TEXT_GENERATION_MODEL = "gpt-6-luna"; |
| 168 | /** Keep the official Antigravity session's current model. Never send this ID to ACP. */ |
| 169 | export const ANTIGRAVITY_DEFAULT_MODEL = "antigravity-default"; |
| 170 | export const DEFAULT_TEXT_GENERATION_REASONING_EFFORT = "low"; |
| 171 | |
| 172 | /** Let Muse pick the account's default model. Never send this ID to Muse. */ |
| 173 | export const MUSE_DEFAULT_MODEL = "default"; |
| 174 | |
| 175 | export 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. */ |
| 190 | export 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 | |
| 200 | export 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 | |
| 228 | export 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 | }; |