import { Effect, Schema, SchemaTransformation } from "effect";import { TrimmedNonEmptyString } from "./baseSchemas.ts";import type { ProviderKind } from "./orchestration.ts"; export const ProviderOptionDescriptorType = Schema.Literals(["select", "boolean"]);export type ProviderOptionDescriptorType = typeof ProviderOptionDescriptorType.Type; export const ProviderOptionChoice = Schema.Struct({ id: TrimmedNonEmptyString, label: TrimmedNonEmptyString, description: Schema.optional(TrimmedNonEmptyString), isDefault: Schema.optional(Schema.Boolean),});export type ProviderOptionChoice = typeof ProviderOptionChoice.Type; const ProviderOptionDescriptorBase = { id: TrimmedNonEmptyString, label: TrimmedNonEmptyString, description: Schema.optional(TrimmedNonEmptyString),} as const; export const SelectProviderOptionDescriptor = Schema.Struct({ ...ProviderOptionDescriptorBase, type: Schema.Literal("select"), options: Schema.Array(ProviderOptionChoice), currentValue: Schema.optional(TrimmedNonEmptyString), promptInjectedValues: Schema.optional(Schema.Array(TrimmedNonEmptyString)),});export type SelectProviderOptionDescriptor = typeof SelectProviderOptionDescriptor.Type; export const BooleanProviderOptionDescriptor = Schema.Struct({ ...ProviderOptionDescriptorBase, type: Schema.Literal("boolean"), currentValue: Schema.optional(Schema.Boolean),});export type BooleanProviderOptionDescriptor = typeof BooleanProviderOptionDescriptor.Type; export const ProviderOptionDescriptor = Schema.Union([ SelectProviderOptionDescriptor, BooleanProviderOptionDescriptor,]);export type ProviderOptionDescriptor = typeof ProviderOptionDescriptor.Type; export const ProviderOptionSelectionValue = Schema.Union([TrimmedNonEmptyString, Schema.Boolean]);export type ProviderOptionSelectionValue = typeof ProviderOptionSelectionValue.Type; export const ProviderOptionSelection = Schema.Struct({ id: TrimmedNonEmptyString, value: ProviderOptionSelectionValue,});export type ProviderOptionSelection = typeof ProviderOptionSelection.Type; /** * Legacy on-disk shape for provider option selections, kept readable by the * decoder so we can tolerate stored data written before the v3 array shape. * * Persisted historically as `{ effort: "max", fastMode: true, ... }` inside * `modelSelection.options`. Migration 026 rewrites stored rows to the * canonical array shape, but we still see the legacy form in: * - `settings.json` files from older client builds, * - SQLite databases that have not yet run migration 026, * - any future regression that re-introduces the legacy shape. */const LegacyProviderOptionSelectionsObject = Schema.Record(Schema.String, Schema.Unknown); const ProviderOptionSelectionsFromLegacyObject = LegacyProviderOptionSelectionsObject.pipe( Schema.decodeTo( Schema.Array(ProviderOptionSelection), SchemaTransformation.transformOrFail({ decode: (record) => Effect.succeed(coerceLegacyOptionsObjectToArray(record)), encode: (selections) => Effect.succeed(canonicalSelectionsToLegacyObject(selections)), }), ),); /** * Schema for the `options` field of every `ModelSelection` variant. * * Accepts both: * - the canonical array shape `Array<{ id, value }>` (preferred), and * - the legacy object shape `Record<string, string | boolean | …>` from * pre-migration data. * * Always normalizes to the canonical array on decode and re-encodes as the * canonical array, so any legacy storage gets cleaned up the next time the * containing record is written back. */export const ProviderOptionSelections = Schema.Union([ Schema.Array(ProviderOptionSelection), ProviderOptionSelectionsFromLegacyObject,]);export type ProviderOptionSelections = typeof ProviderOptionSelections.Type; function coerceLegacyOptionsObjectToArray( record: Record<string, unknown>,): ReadonlyArray<ProviderOptionSelection> { const entries: Array<ProviderOptionSelection> = []; for (const [rawKey, rawValue] of Object.entries(record)) { const id = typeof rawKey === "string" ? rawKey.trim() : ""; if (!id) continue; if (typeof rawValue === "string") { const trimmed = rawValue.trim(); if (trimmed) entries.push({ id, value: trimmed }); } else if (typeof rawValue === "boolean") { entries.push({ id, value: rawValue }); } // Drop anything else (numbers, null, nested objects/arrays) to match the // permissive normalization performed by migration 026. } return entries;} function canonicalSelectionsToLegacyObject( selections: ReadonlyArray<ProviderOptionSelection>,): Record<string, string | boolean> { const out: Record<string, string | boolean> = {}; for (const { id, value } of selections) { out[id] = value; } return out;} export const ModelCapabilities = Schema.Struct({ optionDescriptors: Schema.optional(Schema.Array(ProviderOptionDescriptor)),});export type ModelCapabilities = typeof ModelCapabilities.Type; export const DEFAULT_MODEL_BY_PROVIDER: Record<ProviderKind, string> = { codex: "gpt-5.4", claudeAgent: "claude-sonnet-4-6", cursor: "auto", opencode: "openai/gpt-5",}; export const DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.codex; /** Per-provider text generation model defaults. */export const DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER: Record<ProviderKind, string> = { codex: "gpt-5.4-mini", claudeAgent: "claude-haiku-4-5", cursor: "composer-2", opencode: "openai/gpt-5",}; export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string, string>> = { codex: { "gpt-5-codex": "gpt-5.4", "5.4": "gpt-5.4", "5.3": "gpt-5.3-codex", "gpt-5.3": "gpt-5.3-codex", "5.3-spark": "gpt-5.3-codex-spark", "gpt-5.3-spark": "gpt-5.3-codex-spark", }, claudeAgent: { opus: "claude-opus-4-7", "opus-4.7": "claude-opus-4-7", "claude-opus-4.7": "claude-opus-4-7", "opus-4.6": "claude-opus-4-6", "claude-opus-4.6": "claude-opus-4-6", "claude-opus-4-6-20251117": "claude-opus-4-6", sonnet: "claude-sonnet-4-6", "sonnet-4.6": "claude-sonnet-4-6", "claude-sonnet-4.6": "claude-sonnet-4-6", "claude-sonnet-4-6-20251117": "claude-sonnet-4-6", haiku: "claude-haiku-4-5", "haiku-4.5": "claude-haiku-4-5", "claude-haiku-4.5": "claude-haiku-4-5", "claude-haiku-4-5-20251001": "claude-haiku-4-5", }, cursor: { composer: "composer-2", "composer-1.5": "composer-1.5", "composer-1": "composer-1.5", "opus-4.6-thinking": "claude-opus-4-6", "opus-4.6": "claude-opus-4-6", "sonnet-4.6-thinking": "claude-sonnet-4-6", "sonnet-4.6": "claude-sonnet-4-6", "opus-4.5-thinking": "claude-opus-4-5", "opus-4.5": "claude-opus-4-5", }, opencode: {},}; // ── Provider display names ──────────────────────────────────────────── export const PROVIDER_DISPLAY_NAMES: Record<ProviderKind, string> = { codex: "Codex", claudeAgent: "Claude", cursor: "Cursor", opencode: "OpenCode",};
import { Effect, Schema, SchemaTransformation } from "effect";
import { TrimmedNonEmptyString } from "./baseSchemas.ts";
import type { ProviderKind } from "./orchestration.ts";
export const ProviderOptionDescriptorType = Schema.Literals(["select", "boolean"]);
export type ProviderOptionDescriptorType = typeof ProviderOptionDescriptorType.Type;
export const ProviderOptionChoice = Schema.Struct({
id: TrimmedNonEmptyString,
label: TrimmedNonEmptyString,
description: Schema.optional(TrimmedNonEmptyString),
isDefault: Schema.optional(Schema.Boolean),
});
export type ProviderOptionChoice = typeof ProviderOptionChoice.Type;
const ProviderOptionDescriptorBase = {
id: TrimmedNonEmptyString,
label: TrimmedNonEmptyString,
description: Schema.optional(TrimmedNonEmptyString),
} as const;
export const SelectProviderOptionDescriptor = Schema.Struct({
...ProviderOptionDescriptorBase,
type: Schema.Literal("select"),
options: Schema.Array(ProviderOptionChoice),
currentValue: Schema.optional(TrimmedNonEmptyString),
promptInjectedValues: Schema.optional(Schema.Array(TrimmedNonEmptyString)),
});
export type SelectProviderOptionDescriptor = typeof SelectProviderOptionDescriptor.Type;
export const BooleanProviderOptionDescriptor = Schema.Struct({
...ProviderOptionDescriptorBase,
type: Schema.Literal("boolean"),
currentValue: Schema.optional(Schema.Boolean),
});
export type BooleanProviderOptionDescriptor = typeof BooleanProviderOptionDescriptor.Type;
export const ProviderOptionDescriptor = Schema.Union([
SelectProviderOptionDescriptor,
BooleanProviderOptionDescriptor,
]);
export type ProviderOptionDescriptor = typeof ProviderOptionDescriptor.Type;
export const ProviderOptionSelectionValue = Schema.Union([TrimmedNonEmptyString, Schema.Boolean]);
export type ProviderOptionSelectionValue = typeof ProviderOptionSelectionValue.Type;
export const ProviderOptionSelection = Schema.Struct({
id: TrimmedNonEmptyString,
value: ProviderOptionSelectionValue,
});
export type ProviderOptionSelection = typeof ProviderOptionSelection.Type;
/**
* Legacy on-disk shape for provider option selections, kept readable by the
* decoder so we can tolerate stored data written before the v3 array shape.
*
* Persisted historically as `{ effort: "max", fastMode: true, ... }` inside
* `modelSelection.options`. Migration 026 rewrites stored rows to the
* canonical array shape, but we still see the legacy form in:
* - `settings.json` files from older client builds,
* - SQLite databases that have not yet run migration 026,
* - any future regression that re-introduces the legacy shape.
*/
const LegacyProviderOptionSelectionsObject = Schema.Record(Schema.String, Schema.Unknown);
const ProviderOptionSelectionsFromLegacyObject = LegacyProviderOptionSelectionsObject.pipe(
Schema.decodeTo(
Schema.Array(ProviderOptionSelection),
SchemaTransformation.transformOrFail({
decode: (record) => Effect.succeed(coerceLegacyOptionsObjectToArray(record)),
encode: (selections) => Effect.succeed(canonicalSelectionsToLegacyObject(selections)),
}),
),
);
/**
* Schema for the `options` field of every `ModelSelection` variant.
*
* Accepts both:
* - the canonical array shape `Array<{ id, value }>` (preferred), and
* - the legacy object shape `Record<string, string | boolean | …>` from
* pre-migration data.
*
* Always normalizes to the canonical array on decode and re-encodes as the
* canonical array, so any legacy storage gets cleaned up the next time the
* containing record is written back.
*/
export const ProviderOptionSelections = Schema.Union([
Schema.Array(ProviderOptionSelection),
ProviderOptionSelectionsFromLegacyObject,
]);
export type ProviderOptionSelections = typeof ProviderOptionSelections.Type;
function coerceLegacyOptionsObjectToArray(
record: Record<string, unknown>,
): ReadonlyArray<ProviderOptionSelection> {
const entries: Array<ProviderOptionSelection> = [];
for (const [rawKey, rawValue] of Object.entries(record)) {
const id = typeof rawKey === "string" ? rawKey.trim() : "";
if (!id) continue;
if (typeof rawValue === "string") {
const trimmed = rawValue.trim();
if (trimmed) entries.push({ id, value: trimmed });
} else if (typeof rawValue === "boolean") {
entries.push({ id, value: rawValue });
}
// Drop anything else (numbers, null, nested objects/arrays) to match the
// permissive normalization performed by migration 026.
}
return entries;
}
function canonicalSelectionsToLegacyObject(
selections: ReadonlyArray<ProviderOptionSelection>,
): Record<string, string | boolean> {
const out: Record<string, string | boolean> = {};
for (const { id, value } of selections) {
out[id] = value;
}
return out;
}
export const ModelCapabilities = Schema.Struct({
optionDescriptors: Schema.optional(Schema.Array(ProviderOptionDescriptor)),
});
export type ModelCapabilities = typeof ModelCapabilities.Type;
export const DEFAULT_MODEL_BY_PROVIDER: Record<ProviderKind, string> = {
codex: "gpt-5.4",
claudeAgent: "claude-sonnet-4-6",
cursor: "auto",
opencode: "openai/gpt-5",
};
export const DEFAULT_MODEL = DEFAULT_MODEL_BY_PROVIDER.codex;
/** Per-provider text generation model defaults. */
export const DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER: Record<ProviderKind, string> = {
codex: "gpt-5.4-mini",
claudeAgent: "claude-haiku-4-5",
cursor: "composer-2",
opencode: "openai/gpt-5",
};
export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record<ProviderKind, Record<string, string>> = {
codex: {
"gpt-5-codex": "gpt-5.4",
"5.4": "gpt-5.4",
"5.3": "gpt-5.3-codex",
"gpt-5.3": "gpt-5.3-codex",
"5.3-spark": "gpt-5.3-codex-spark",
"gpt-5.3-spark": "gpt-5.3-codex-spark",
},
claudeAgent: {
opus: "claude-opus-4-7",
"opus-4.7": "claude-opus-4-7",
"claude-opus-4.7": "claude-opus-4-7",
"opus-4.6": "claude-opus-4-6",
"claude-opus-4.6": "claude-opus-4-6",
"claude-opus-4-6-20251117": "claude-opus-4-6",
sonnet: "claude-sonnet-4-6",
"sonnet-4.6": "claude-sonnet-4-6",
"claude-sonnet-4.6": "claude-sonnet-4-6",
"claude-sonnet-4-6-20251117": "claude-sonnet-4-6",
haiku: "claude-haiku-4-5",
"haiku-4.5": "claude-haiku-4-5",
"claude-haiku-4.5": "claude-haiku-4-5",
"claude-haiku-4-5-20251001": "claude-haiku-4-5",
},
cursor: {
composer: "composer-2",
"composer-1.5": "composer-1.5",
"composer-1": "composer-1.5",
"opus-4.6-thinking": "claude-opus-4-6",
"opus-4.6": "claude-opus-4-6",
"sonnet-4.6-thinking": "claude-sonnet-4-6",
"sonnet-4.6": "claude-sonnet-4-6",
"opus-4.5-thinking": "claude-opus-4-5",
"opus-4.5": "claude-opus-4-5",
},
opencode: {},
};
// ── Provider display names ────────────────────────────────────────────
export const PROVIDER_DISPLAY_NAMES: Record<ProviderKind, string> = {
codex: "Codex",
claudeAgent: "Claude",
cursor: "Cursor",
opencode: "OpenCode",
};