@oh-my-pi/pi-coding-agent 14.0.5 → 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 (40) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/package.json +7 -7
  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 +179 -11
  9. package/src/config/model-resolver.ts +171 -50
  10. package/src/config/settings-schema.ts +23 -0
  11. package/src/export/html/template.css +82 -0
  12. package/src/export/html/template.generated.ts +1 -1
  13. package/src/export/html/template.js +612 -97
  14. package/src/internal-urls/docs-index.generated.ts +1 -1
  15. package/src/internal-urls/jobs-protocol.ts +2 -1
  16. package/src/lsp/client.ts +1 -1
  17. package/src/main.ts +6 -1
  18. package/src/memories/index.ts +7 -6
  19. package/src/modes/components/model-selector.ts +221 -64
  20. package/src/modes/controllers/command-controller.ts +18 -0
  21. package/src/modes/controllers/selector-controller.ts +13 -5
  22. package/src/prompts/system/system-prompt.md +5 -1
  23. package/src/prompts/tools/bash.md +15 -0
  24. package/src/prompts/tools/cancel-job.md +1 -1
  25. package/src/prompts/tools/read-chunk.md +9 -0
  26. package/src/prompts/tools/read.md +9 -0
  27. package/src/prompts/tools/write.md +1 -0
  28. package/src/sdk.ts +7 -4
  29. package/src/session/agent-session.ts +23 -6
  30. package/src/task/executor.ts +5 -1
  31. package/src/tools/await-tool.ts +2 -1
  32. package/src/tools/bash.ts +221 -56
  33. package/src/tools/cancel-job.ts +2 -1
  34. package/src/tools/inspect-image.ts +1 -1
  35. package/src/tools/read.ts +218 -1
  36. package/src/tools/sqlite-reader.ts +623 -0
  37. package/src/tools/write.ts +187 -1
  38. package/src/utils/commit-message-generator.ts +1 -0
  39. package/src/utils/git.ts +24 -1
  40. package/src/utils/title-generator.ts +1 -1
@@ -31,8 +31,18 @@ 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 {
@@ -263,8 +273,14 @@ const ProviderConfigSchema = Type.Object({
263
273
  modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),
264
274
  });
265
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
+
266
281
  const ModelsConfigSchema = Type.Object({
267
- providers: Type.Record(Type.String(), ProviderConfigSchema),
282
+ providers: Type.Optional(Type.Record(Type.String(), ProviderConfigSchema)),
283
+ equivalence: Type.Optional(EquivalenceConfigSchema),
268
284
  });
269
285
 
270
286
  type ModelsConfig = Static<typeof ModelsConfigSchema>;
@@ -356,7 +372,7 @@ function validateProviderConfiguration(
356
372
  export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsConfigSchema).withValidation(
357
373
  "models",
358
374
  config => {
359
- for (const [providerName, providerConfig] of Object.entries(config.providers)) {
375
+ for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
360
376
  validateProviderConfiguration(
361
377
  providerName,
362
378
  {
@@ -405,6 +421,11 @@ export interface ProviderDiscoveryState {
405
421
  error?: string;
406
422
  }
407
423
 
424
+ export interface CanonicalModelQueryOptions {
425
+ availableOnly?: boolean;
426
+ candidates?: readonly Model<Api>[];
427
+ }
428
+
408
429
  /** Result of loading custom models from models.json */
409
430
  interface CustomModelsResult {
410
431
  models?: CustomModelOverlay[];
@@ -413,6 +434,7 @@ interface CustomModelsResult {
413
434
  keylessProviders?: Set<string>;
414
435
  discoverableProviders?: DiscoveryProviderConfig[];
415
436
  configuredProviders?: Set<string>;
437
+ equivalence?: ModelEquivalenceConfig;
416
438
  error?: ConfigError;
417
439
  found: boolean;
418
440
  }
@@ -739,17 +761,27 @@ function getDisabledProviderIdsFromSettings(): Set<string> {
739
761
  }
740
762
  }
741
763
 
764
+ function getConfiguredProviderOrderFromSettings(): string[] {
765
+ try {
766
+ return settings.get("modelProviderOrder");
767
+ } catch {
768
+ return [];
769
+ }
770
+ }
771
+
742
772
  /**
743
773
  * Model registry - loads and manages models, resolves API keys via AuthStorage.
744
774
  */
745
775
  export class ModelRegistry {
746
776
  #models: Model<Api>[] = [];
777
+ #canonicalIndex: CanonicalModelIndex = { records: [], byId: new Map(), bySelector: new Map() };
747
778
  #customProviderApiKeys: Map<string, string> = new Map();
748
779
  #keylessProviders: Set<string> = new Set();
749
780
  #discoverableProviders: DiscoveryProviderConfig[] = [];
750
781
  #customModelOverlays: CustomModelOverlay[] = [];
751
782
  #providerOverrides: Map<string, ProviderOverride> = new Map();
752
783
  #modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
784
+ #equivalenceConfig: ModelEquivalenceConfig | undefined;
753
785
  #configError: ConfigError | undefined = undefined;
754
786
  #modelsConfigFile: ConfigFile<ModelsConfig>;
755
787
  #registeredProviderSources: Set<string> = new Set();
@@ -824,6 +856,7 @@ export class ModelRegistry {
824
856
  this.#discoverableProviders = [];
825
857
  this.#providerOverrides.clear();
826
858
  this.#modelOverrides.clear();
859
+ this.#equivalenceConfig = undefined;
827
860
  this.#configError = undefined;
828
861
  this.#providerDiscoveryStates.clear();
829
862
  this.#loadModels();
@@ -845,6 +878,7 @@ export class ModelRegistry {
845
878
  keylessProviders = new Set(),
846
879
  discoverableProviders = [],
847
880
  configuredProviders = new Set(),
881
+ equivalence,
848
882
  error: configError,
849
883
  } = this.#loadCustomModels();
850
884
  this.#configError = configError;
@@ -853,6 +887,7 @@ export class ModelRegistry {
853
887
  this.#customModelOverlays = customModels;
854
888
  this.#providerOverrides = overrides;
855
889
  this.#modelOverrides = modelOverrides;
890
+ this.#equivalenceConfig = equivalence;
856
891
 
857
892
  this.#addImplicitDiscoverableProviders(configuredProviders);
858
893
  const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
@@ -861,6 +896,7 @@ export class ModelRegistry {
861
896
  const combined = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
862
897
 
863
898
  this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
899
+ this.#rebuildCanonicalIndex();
864
900
  }
865
901
 
866
902
  /** Load built-in models, applying provider-level overrides only.
@@ -1045,9 +1081,10 @@ export class ModelRegistry {
1045
1081
  const allModelOverrides = new Map<string, Map<string, ModelOverride>>();
1046
1082
  const keylessProviders = new Set<string>();
1047
1083
  const discoverableProviders: DiscoveryProviderConfig[] = [];
1048
- const configuredProviders = new Set(Object.keys(value.providers));
1084
+ const providerEntries = Object.entries(value.providers ?? {});
1085
+ const configuredProviders = new Set(Object.keys(value.providers ?? {}));
1049
1086
 
1050
- for (const [providerName, providerConfig] of Object.entries(value.providers)) {
1087
+ for (const [providerName, providerConfig] of providerEntries) {
1051
1088
  // Always set overrides when baseUrl/headers/apiKey/compat are present
1052
1089
  if (providerConfig.baseUrl || providerConfig.headers || providerConfig.apiKey || providerConfig.compat) {
1053
1090
  overrides.set(providerName, {
@@ -1097,6 +1134,7 @@ export class ModelRegistry {
1097
1134
  keylessProviders,
1098
1135
  discoverableProviders,
1099
1136
  configuredProviders,
1137
+ equivalence: value.equivalence,
1100
1138
  found: true,
1101
1139
  };
1102
1140
  }
@@ -1147,6 +1185,7 @@ export class ModelRegistry {
1147
1185
  const resolved = this.#mergeResolvedModels(this.#models, discoveredModels);
1148
1186
  const combined = this.#mergeCustomModels(resolved, this.#customModelOverlays);
1149
1187
  this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
1188
+ this.#rebuildCanonicalIndex();
1150
1189
  }
1151
1190
 
1152
1191
  async #discoverProviderModels(
@@ -1653,10 +1692,14 @@ export class ModelRegistry {
1653
1692
  });
1654
1693
  }
1655
1694
 
1695
+ #rebuildCanonicalIndex(): void {
1696
+ this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig);
1697
+ }
1698
+
1656
1699
  #parseModels(config: ModelsConfig): CustomModelOverlay[] {
1657
1700
  const models: CustomModelOverlay[] = [];
1658
1701
 
1659
- for (const [providerName, providerConfig] of Object.entries(config.providers)) {
1702
+ for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
1660
1703
  const modelDefs = providerConfig.models ?? [];
1661
1704
  if (modelDefs.length === 0) continue; // Override-only, no custom models
1662
1705
  if (providerConfig.apiKey) {
@@ -1688,17 +1731,139 @@ export class ModelRegistry {
1688
1731
  return this.#models;
1689
1732
  }
1690
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
+
1691
1861
  /**
1692
1862
  * Get only models that have auth configured.
1693
1863
  * This is a fast check that doesn't refresh OAuth tokens.
1694
1864
  */
1695
1865
  getAvailable(): Model<Api>[] {
1696
- const disabledProviders = getDisabledProviderIdsFromSettings();
1697
- return this.#models.filter(
1698
- m =>
1699
- !disabledProviders.has(m.provider) &&
1700
- (this.#keylessProviders.has(m.provider) || this.authStorage.hasAuth(m.provider)),
1701
- );
1866
+ return this.#models.filter(model => this.#isModelAvailable(model));
1702
1867
  }
1703
1868
 
1704
1869
  getDiscoverableProviders(): string[] {
@@ -1853,11 +2018,13 @@ export class ModelRegistry {
1853
2018
  const credential = this.authStorage.getOAuthCredential(providerName);
1854
2019
  if (credential) {
1855
2020
  this.#models = config.oauth.modifyModels(nextModels, credential);
2021
+ this.#rebuildCanonicalIndex();
1856
2022
  return;
1857
2023
  }
1858
2024
  }
1859
2025
 
1860
2026
  this.#models = nextModels;
2027
+ this.#rebuildCanonicalIndex();
1861
2028
  return;
1862
2029
  }
1863
2030
 
@@ -1870,6 +2037,7 @@ export class ModelRegistry {
1870
2037
  headers: config.headers ? { ...m.headers, ...config.headers } : m.headers,
1871
2038
  };
1872
2039
  });
2040
+ this.#rebuildCanonicalIndex();
1873
2041
  }
1874
2042
  }
1875
2043