@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.
- package/CHANGELOG.md +41 -0
- package/package.json +7 -7
- package/src/async/index.ts +1 -0
- package/src/async/support.ts +5 -0
- package/src/cli/list-models.ts +96 -57
- package/src/commit/model-selection.ts +16 -13
- package/src/config/model-equivalence.ts +674 -0
- package/src/config/model-registry.ts +179 -11
- package/src/config/model-resolver.ts +171 -50
- package/src/config/settings-schema.ts +23 -0
- package/src/export/html/template.css +82 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +612 -97
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/jobs-protocol.ts +2 -1
- package/src/lsp/client.ts +1 -1
- package/src/main.ts +6 -1
- package/src/memories/index.ts +7 -6
- package/src/modes/components/model-selector.ts +221 -64
- package/src/modes/controllers/command-controller.ts +18 -0
- package/src/modes/controllers/selector-controller.ts +13 -5
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/bash.md +15 -0
- package/src/prompts/tools/cancel-job.md +1 -1
- package/src/prompts/tools/read-chunk.md +9 -0
- package/src/prompts/tools/read.md +9 -0
- package/src/prompts/tools/write.md +1 -0
- package/src/sdk.ts +7 -4
- package/src/session/agent-session.ts +23 -6
- package/src/task/executor.ts +5 -1
- package/src/tools/await-tool.ts +2 -1
- package/src/tools/bash.ts +221 -56
- package/src/tools/cancel-job.ts +2 -1
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/read.ts +218 -1
- package/src/tools/sqlite-reader.ts +623 -0
- package/src/tools/write.ts +187 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +24 -1
- 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
|
|
1084
|
+
const providerEntries = Object.entries(value.providers ?? {});
|
|
1085
|
+
const configuredProviders = new Set(Object.keys(value.providers ?? {}));
|
|
1049
1086
|
|
|
1050
|
-
for (const [providerName, providerConfig] of
|
|
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
|
-
|
|
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
|
|