@oh-my-pi/pi-coding-agent 14.0.4 → 14.1.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/package.json +11 -8
  3. package/src/async/index.ts +1 -0
  4. package/src/async/support.ts +5 -0
  5. package/src/cli/list-models.ts +96 -57
  6. package/src/commit/model-selection.ts +16 -13
  7. package/src/config/model-equivalence.ts +674 -0
  8. package/src/config/model-registry.ts +182 -13
  9. package/src/config/model-resolver.ts +203 -74
  10. package/src/config/settings-schema.ts +23 -0
  11. package/src/config/settings.ts +9 -2
  12. package/src/dap/session.ts +31 -39
  13. package/src/debug/log-formatting.ts +2 -2
  14. package/src/edit/modes/chunk.ts +8 -3
  15. package/src/export/html/template.css +82 -0
  16. package/src/export/html/template.generated.ts +1 -1
  17. package/src/export/html/template.js +612 -97
  18. package/src/internal-urls/docs-index.generated.ts +1 -1
  19. package/src/internal-urls/jobs-protocol.ts +2 -1
  20. package/src/lsp/client.ts +5 -3
  21. package/src/lsp/index.ts +4 -9
  22. package/src/lsp/utils.ts +26 -0
  23. package/src/main.ts +6 -1
  24. package/src/memories/index.ts +7 -6
  25. package/src/modes/components/diff.ts +1 -1
  26. package/src/modes/components/model-selector.ts +221 -64
  27. package/src/modes/controllers/command-controller.ts +18 -0
  28. package/src/modes/controllers/event-controller.ts +438 -426
  29. package/src/modes/controllers/selector-controller.ts +13 -5
  30. package/src/modes/theme/mermaid-cache.ts +5 -7
  31. package/src/priority.json +8 -0
  32. package/src/prompts/agents/designer.md +1 -2
  33. package/src/prompts/system/system-prompt.md +5 -1
  34. package/src/prompts/tools/bash.md +15 -0
  35. package/src/prompts/tools/cancel-job.md +1 -1
  36. package/src/prompts/tools/chunk-edit.md +39 -40
  37. package/src/prompts/tools/read-chunk.md +13 -1
  38. package/src/prompts/tools/read.md +9 -0
  39. package/src/prompts/tools/write.md +1 -0
  40. package/src/sdk.ts +7 -4
  41. package/src/session/agent-session.ts +33 -6
  42. package/src/session/compaction/compaction.ts +1 -1
  43. package/src/task/executor.ts +5 -1
  44. package/src/tools/await-tool.ts +2 -1
  45. package/src/tools/bash.ts +221 -56
  46. package/src/tools/browser.ts +84 -21
  47. package/src/tools/cancel-job.ts +2 -1
  48. package/src/tools/fetch.ts +1 -1
  49. package/src/tools/find.ts +40 -94
  50. package/src/tools/gemini-image.ts +1 -0
  51. package/src/tools/inspect-image.ts +1 -1
  52. package/src/tools/read.ts +218 -1
  53. package/src/tools/render-utils.ts +1 -1
  54. package/src/tools/sqlite-reader.ts +623 -0
  55. package/src/tools/write.ts +187 -1
  56. package/src/utils/commit-message-generator.ts +1 -0
  57. package/src/utils/git.ts +24 -1
  58. package/src/utils/image-resize.ts +73 -37
  59. package/src/utils/title-generator.ts +1 -1
  60. package/src/web/scrapers/types.ts +50 -32
  61. package/src/web/search/providers/codex.ts +21 -2
@@ -31,15 +31,25 @@ import { type ConfigError, ConfigFile } from "../config";
31
31
  import { parseModelString } from "../config/model-resolver";
32
32
  import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
33
33
  import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
34
+ import {
35
+ buildCanonicalModelIndex,
36
+ type CanonicalModelIndex,
37
+ type CanonicalModelRecord,
38
+ type CanonicalModelVariant,
39
+ formatCanonicalVariantSelector,
40
+ type ModelEquivalenceConfig,
41
+ } from "./model-equivalence";
34
42
  import { type Settings, settings } from "./settings";
35
43
 
44
+ export type { CanonicalModelIndex, CanonicalModelRecord, CanonicalModelVariant, ModelEquivalenceConfig };
45
+
36
46
  export const kNoAuth = "N/A";
37
47
 
38
48
  export function isAuthenticated(apiKey: string | undefined | null): apiKey is string {
39
49
  return Boolean(apiKey) && apiKey !== kNoAuth;
40
50
  }
41
51
 
42
- export type ModelRole = "default" | "smol" | "slow" | "vision" | "plan" | "commit" | "task";
52
+ export type ModelRole = "default" | "smol" | "slow" | "vision" | "plan" | "designer" | "commit" | "task";
43
53
 
44
54
  export interface ModelRoleInfo {
45
55
  tag?: string;
@@ -53,11 +63,12 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
53
63
  slow: { tag: "SLOW", name: "Thinking", color: "accent" },
54
64
  vision: { tag: "VISION", name: "Vision", color: "error" },
55
65
  plan: { tag: "PLAN", name: "Architect", color: "muted" },
66
+ designer: { tag: "DESIGNER", name: "Designer", color: "muted" },
56
67
  commit: { tag: "COMMIT", name: "Commit", color: "dim" },
57
68
  task: { tag: "TASK", name: "Subtask", color: "muted" },
58
69
  };
59
70
 
60
- export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "vision", "plan", "commit", "task"];
71
+ export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "vision", "plan", "designer", "commit", "task"];
61
72
 
62
73
  /** Alias for ModelRoleInfo - used for both built-in and custom roles */
63
74
  export type RoleInfo = ModelRoleInfo;
@@ -262,8 +273,14 @@ const ProviderConfigSchema = Type.Object({
262
273
  modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),
263
274
  });
264
275
 
276
+ const EquivalenceConfigSchema = Type.Object({
277
+ overrides: Type.Optional(Type.Record(Type.String(), Type.String({ minLength: 1 }))),
278
+ exclude: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
279
+ });
280
+
265
281
  const ModelsConfigSchema = Type.Object({
266
- providers: Type.Record(Type.String(), ProviderConfigSchema),
282
+ providers: Type.Optional(Type.Record(Type.String(), ProviderConfigSchema)),
283
+ equivalence: Type.Optional(EquivalenceConfigSchema),
267
284
  });
268
285
 
269
286
  type ModelsConfig = Static<typeof ModelsConfigSchema>;
@@ -355,7 +372,7 @@ function validateProviderConfiguration(
355
372
  export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsConfigSchema).withValidation(
356
373
  "models",
357
374
  config => {
358
- for (const [providerName, providerConfig] of Object.entries(config.providers)) {
375
+ for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
359
376
  validateProviderConfiguration(
360
377
  providerName,
361
378
  {
@@ -404,6 +421,11 @@ export interface ProviderDiscoveryState {
404
421
  error?: string;
405
422
  }
406
423
 
424
+ export interface CanonicalModelQueryOptions {
425
+ availableOnly?: boolean;
426
+ candidates?: readonly Model<Api>[];
427
+ }
428
+
407
429
  /** Result of loading custom models from models.json */
408
430
  interface CustomModelsResult {
409
431
  models?: CustomModelOverlay[];
@@ -412,6 +434,7 @@ interface CustomModelsResult {
412
434
  keylessProviders?: Set<string>;
413
435
  discoverableProviders?: DiscoveryProviderConfig[];
414
436
  configuredProviders?: Set<string>;
437
+ equivalence?: ModelEquivalenceConfig;
415
438
  error?: ConfigError;
416
439
  found: boolean;
417
440
  }
@@ -738,17 +761,27 @@ function getDisabledProviderIdsFromSettings(): Set<string> {
738
761
  }
739
762
  }
740
763
 
764
+ function getConfiguredProviderOrderFromSettings(): string[] {
765
+ try {
766
+ return settings.get("modelProviderOrder");
767
+ } catch {
768
+ return [];
769
+ }
770
+ }
771
+
741
772
  /**
742
773
  * Model registry - loads and manages models, resolves API keys via AuthStorage.
743
774
  */
744
775
  export class ModelRegistry {
745
776
  #models: Model<Api>[] = [];
777
+ #canonicalIndex: CanonicalModelIndex = { records: [], byId: new Map(), bySelector: new Map() };
746
778
  #customProviderApiKeys: Map<string, string> = new Map();
747
779
  #keylessProviders: Set<string> = new Set();
748
780
  #discoverableProviders: DiscoveryProviderConfig[] = [];
749
781
  #customModelOverlays: CustomModelOverlay[] = [];
750
782
  #providerOverrides: Map<string, ProviderOverride> = new Map();
751
783
  #modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
784
+ #equivalenceConfig: ModelEquivalenceConfig | undefined;
752
785
  #configError: ConfigError | undefined = undefined;
753
786
  #modelsConfigFile: ConfigFile<ModelsConfig>;
754
787
  #registeredProviderSources: Set<string> = new Set();
@@ -823,6 +856,7 @@ export class ModelRegistry {
823
856
  this.#discoverableProviders = [];
824
857
  this.#providerOverrides.clear();
825
858
  this.#modelOverrides.clear();
859
+ this.#equivalenceConfig = undefined;
826
860
  this.#configError = undefined;
827
861
  this.#providerDiscoveryStates.clear();
828
862
  this.#loadModels();
@@ -844,6 +878,7 @@ export class ModelRegistry {
844
878
  keylessProviders = new Set(),
845
879
  discoverableProviders = [],
846
880
  configuredProviders = new Set(),
881
+ equivalence,
847
882
  error: configError,
848
883
  } = this.#loadCustomModels();
849
884
  this.#configError = configError;
@@ -852,6 +887,7 @@ export class ModelRegistry {
852
887
  this.#customModelOverlays = customModels;
853
888
  this.#providerOverrides = overrides;
854
889
  this.#modelOverrides = modelOverrides;
890
+ this.#equivalenceConfig = equivalence;
855
891
 
856
892
  this.#addImplicitDiscoverableProviders(configuredProviders);
857
893
  const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
@@ -860,6 +896,7 @@ export class ModelRegistry {
860
896
  const combined = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
861
897
 
862
898
  this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
899
+ this.#rebuildCanonicalIndex();
863
900
  }
864
901
 
865
902
  /** Load built-in models, applying provider-level overrides only.
@@ -1044,9 +1081,10 @@ export class ModelRegistry {
1044
1081
  const allModelOverrides = new Map<string, Map<string, ModelOverride>>();
1045
1082
  const keylessProviders = new Set<string>();
1046
1083
  const discoverableProviders: DiscoveryProviderConfig[] = [];
1047
- const configuredProviders = new Set(Object.keys(value.providers));
1084
+ const providerEntries = Object.entries(value.providers ?? {});
1085
+ const configuredProviders = new Set(Object.keys(value.providers ?? {}));
1048
1086
 
1049
- for (const [providerName, providerConfig] of Object.entries(value.providers)) {
1087
+ for (const [providerName, providerConfig] of providerEntries) {
1050
1088
  // Always set overrides when baseUrl/headers/apiKey/compat are present
1051
1089
  if (providerConfig.baseUrl || providerConfig.headers || providerConfig.apiKey || providerConfig.compat) {
1052
1090
  overrides.set(providerName, {
@@ -1096,6 +1134,7 @@ export class ModelRegistry {
1096
1134
  keylessProviders,
1097
1135
  discoverableProviders,
1098
1136
  configuredProviders,
1137
+ equivalence: value.equivalence,
1099
1138
  found: true,
1100
1139
  };
1101
1140
  }
@@ -1146,6 +1185,7 @@ export class ModelRegistry {
1146
1185
  const resolved = this.#mergeResolvedModels(this.#models, discoveredModels);
1147
1186
  const combined = this.#mergeCustomModels(resolved, this.#customModelOverlays);
1148
1187
  this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
1188
+ this.#rebuildCanonicalIndex();
1149
1189
  }
1150
1190
 
1151
1191
  async #discoverProviderModels(
@@ -1652,10 +1692,14 @@ export class ModelRegistry {
1652
1692
  });
1653
1693
  }
1654
1694
 
1695
+ #rebuildCanonicalIndex(): void {
1696
+ this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig);
1697
+ }
1698
+
1655
1699
  #parseModels(config: ModelsConfig): CustomModelOverlay[] {
1656
1700
  const models: CustomModelOverlay[] = [];
1657
1701
 
1658
- for (const [providerName, providerConfig] of Object.entries(config.providers)) {
1702
+ for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
1659
1703
  const modelDefs = providerConfig.models ?? [];
1660
1704
  if (modelDefs.length === 0) continue; // Override-only, no custom models
1661
1705
  if (providerConfig.apiKey) {
@@ -1687,17 +1731,139 @@ export class ModelRegistry {
1687
1731
  return this.#models;
1688
1732
  }
1689
1733
 
1734
+ #isModelAvailable(model: Model<Api>): boolean {
1735
+ const disabledProviders = getDisabledProviderIdsFromSettings();
1736
+ return (
1737
+ !disabledProviders.has(model.provider) &&
1738
+ (this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider))
1739
+ );
1740
+ }
1741
+
1742
+ #filterCanonicalVariants(
1743
+ record: CanonicalModelRecord,
1744
+ options: CanonicalModelQueryOptions | undefined,
1745
+ ): CanonicalModelVariant[] {
1746
+ const candidateKeys = options?.candidates
1747
+ ? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
1748
+ : undefined;
1749
+ return record.variants.filter(variant => {
1750
+ if (candidateKeys && !candidateKeys.has(variant.selector)) {
1751
+ return false;
1752
+ }
1753
+ if (options?.availableOnly && !this.#isModelAvailable(variant.model)) {
1754
+ return false;
1755
+ }
1756
+ return true;
1757
+ });
1758
+ }
1759
+
1760
+ #providerRank(models: readonly Model<Api>[]): Map<string, number> {
1761
+ const configuredProviders = getConfiguredProviderOrderFromSettings();
1762
+ const result = new Map<string, number>();
1763
+ let nextRank = 0;
1764
+ for (const provider of configuredProviders) {
1765
+ const normalized = provider.trim().toLowerCase();
1766
+ if (!normalized || result.has(normalized)) {
1767
+ continue;
1768
+ }
1769
+ result.set(normalized, nextRank);
1770
+ nextRank += 1;
1771
+ }
1772
+ for (const model of models) {
1773
+ const normalized = model.provider.toLowerCase();
1774
+ if (result.has(normalized)) {
1775
+ continue;
1776
+ }
1777
+ result.set(normalized, nextRank);
1778
+ nextRank += 1;
1779
+ }
1780
+ return result;
1781
+ }
1782
+
1783
+ #resolveCanonicalVariant(
1784
+ variants: readonly CanonicalModelVariant[],
1785
+ allCandidates: readonly Model<Api>[],
1786
+ ): CanonicalModelVariant | undefined {
1787
+ if (variants.length === 0) {
1788
+ return undefined;
1789
+ }
1790
+ const providerRank = this.#providerRank(allCandidates);
1791
+ const modelOrder = new Map<string, number>();
1792
+ for (let index = 0; index < allCandidates.length; index += 1) {
1793
+ modelOrder.set(formatCanonicalVariantSelector(allCandidates[index]!), index);
1794
+ }
1795
+ const sourceRank: Record<CanonicalModelVariant["source"], number> = {
1796
+ override: 1,
1797
+ bundled: 1,
1798
+ heuristic: 2,
1799
+ fallback: 3,
1800
+ };
1801
+ return [...variants].sort((left, right) => {
1802
+ const leftProviderRank = providerRank.get(left.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
1803
+ const rightProviderRank = providerRank.get(right.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
1804
+ if (leftProviderRank !== rightProviderRank) {
1805
+ return leftProviderRank - rightProviderRank;
1806
+ }
1807
+ const leftExact = left.model.id === left.canonicalId ? 0 : 1;
1808
+ const rightExact = right.model.id === right.canonicalId ? 0 : 1;
1809
+ if (leftExact !== rightExact) {
1810
+ return leftExact - rightExact;
1811
+ }
1812
+ if (sourceRank[left.source] !== sourceRank[right.source]) {
1813
+ return sourceRank[left.source] - sourceRank[right.source];
1814
+ }
1815
+ if (left.model.id.length !== right.model.id.length) {
1816
+ return left.model.id.length - right.model.id.length;
1817
+ }
1818
+ const leftOrder = modelOrder.get(left.selector) ?? Number.MAX_SAFE_INTEGER;
1819
+ const rightOrder = modelOrder.get(right.selector) ?? Number.MAX_SAFE_INTEGER;
1820
+ return leftOrder - rightOrder;
1821
+ })[0];
1822
+ }
1823
+
1824
+ getCanonicalModels(options?: CanonicalModelQueryOptions): CanonicalModelRecord[] {
1825
+ const records: CanonicalModelRecord[] = [];
1826
+ for (const record of this.#canonicalIndex.records) {
1827
+ const variants = this.#filterCanonicalVariants(record, options);
1828
+ if (variants.length === 0) {
1829
+ continue;
1830
+ }
1831
+ records.push({
1832
+ id: record.id,
1833
+ name: record.name,
1834
+ variants,
1835
+ });
1836
+ }
1837
+ return records;
1838
+ }
1839
+
1840
+ getCanonicalVariants(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[] {
1841
+ const record = this.#canonicalIndex.byId.get(canonicalId.trim().toLowerCase());
1842
+ if (!record) {
1843
+ return [];
1844
+ }
1845
+ return this.#filterCanonicalVariants(record, options);
1846
+ }
1847
+
1848
+ resolveCanonicalModel(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined {
1849
+ const variants = this.getCanonicalVariants(canonicalId, options);
1850
+ if (variants.length === 0) {
1851
+ return undefined;
1852
+ }
1853
+ const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
1854
+ return this.#resolveCanonicalVariant(variants, candidates)?.model;
1855
+ }
1856
+
1857
+ getCanonicalId(model: Model<Api>): string | undefined {
1858
+ return this.#canonicalIndex.bySelector.get(formatCanonicalVariantSelector(model).toLowerCase());
1859
+ }
1860
+
1690
1861
  /**
1691
1862
  * Get only models that have auth configured.
1692
1863
  * This is a fast check that doesn't refresh OAuth tokens.
1693
1864
  */
1694
1865
  getAvailable(): Model<Api>[] {
1695
- const disabledProviders = getDisabledProviderIdsFromSettings();
1696
- return this.#models.filter(
1697
- m =>
1698
- !disabledProviders.has(m.provider) &&
1699
- (this.#keylessProviders.has(m.provider) || this.authStorage.hasAuth(m.provider)),
1700
- );
1866
+ return this.#models.filter(model => this.#isModelAvailable(model));
1701
1867
  }
1702
1868
 
1703
1869
  getDiscoverableProviders(): string[] {
@@ -1852,11 +2018,13 @@ export class ModelRegistry {
1852
2018
  const credential = this.authStorage.getOAuthCredential(providerName);
1853
2019
  if (credential) {
1854
2020
  this.#models = config.oauth.modifyModels(nextModels, credential);
2021
+ this.#rebuildCanonicalIndex();
1855
2022
  return;
1856
2023
  }
1857
2024
  }
1858
2025
 
1859
2026
  this.#models = nextModels;
2027
+ this.#rebuildCanonicalIndex();
1860
2028
  return;
1861
2029
  }
1862
2030
 
@@ -1869,6 +2037,7 @@ export class ModelRegistry {
1869
2037
  headers: config.headers ? { ...m.headers, ...config.headers } : m.headers,
1870
2038
  };
1871
2039
  });
2040
+ this.#rebuildCanonicalIndex();
1872
2041
  }
1873
2042
  }
1874
2043