@oh-my-pi/pi-coding-agent 14.0.5 → 14.1.1

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 (101) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/package.json +8 -8
  3. package/src/async/index.ts +1 -0
  4. package/src/async/job-manager.ts +43 -10
  5. package/src/async/support.ts +5 -0
  6. package/src/cli/list-models.ts +96 -57
  7. package/src/commit/agentic/tools/analyze-file.ts +1 -2
  8. package/src/commit/model-selection.ts +16 -13
  9. package/src/config/mcp-schema.json +1 -1
  10. package/src/config/model-equivalence.ts +675 -0
  11. package/src/config/model-registry.ts +242 -45
  12. package/src/config/model-resolver.ts +282 -65
  13. package/src/config/settings-schema.ts +27 -3
  14. package/src/config/settings.ts +1 -1
  15. package/src/cursor.ts +64 -23
  16. package/src/edit/index.ts +254 -89
  17. package/src/edit/modes/chunk.ts +336 -57
  18. package/src/edit/modes/hashline.ts +51 -26
  19. package/src/edit/modes/patch.ts +16 -10
  20. package/src/edit/modes/replace.ts +15 -7
  21. package/src/edit/renderer.ts +248 -94
  22. package/src/export/html/template.css +82 -0
  23. package/src/export/html/template.generated.ts +1 -1
  24. package/src/export/html/template.js +614 -97
  25. package/src/extensibility/custom-tools/types.ts +0 -3
  26. package/src/extensibility/extensions/loader.ts +16 -0
  27. package/src/extensibility/extensions/runner.ts +2 -7
  28. package/src/extensibility/extensions/types.ts +8 -4
  29. package/src/internal-urls/docs-index.generated.ts +4 -4
  30. package/src/internal-urls/jobs-protocol.ts +2 -1
  31. package/src/ipy/executor.ts +447 -52
  32. package/src/ipy/kernel.ts +39 -13
  33. package/src/lsp/client.ts +55 -1
  34. package/src/lsp/index.ts +8 -0
  35. package/src/lsp/types.ts +6 -0
  36. package/src/main.ts +6 -2
  37. package/src/memories/index.ts +7 -6
  38. package/src/modes/acp/acp-agent.ts +4 -1
  39. package/src/modes/components/bash-execution.ts +16 -4
  40. package/src/modes/components/model-selector.ts +221 -64
  41. package/src/modes/components/status-line/presets.ts +17 -6
  42. package/src/modes/components/status-line/segments.ts +15 -0
  43. package/src/modes/components/status-line-segment-editor.ts +1 -0
  44. package/src/modes/components/status-line.ts +7 -1
  45. package/src/modes/components/tool-execution.ts +145 -75
  46. package/src/modes/controllers/command-controller.ts +42 -1
  47. package/src/modes/controllers/event-controller.ts +4 -1
  48. package/src/modes/controllers/extension-ui-controller.ts +28 -5
  49. package/src/modes/controllers/input-controller.ts +9 -3
  50. package/src/modes/controllers/selector-controller.ts +17 -6
  51. package/src/modes/interactive-mode.ts +19 -3
  52. package/src/modes/print-mode.ts +13 -4
  53. package/src/modes/prompt-action-autocomplete.ts +3 -5
  54. package/src/modes/rpc/rpc-mode.ts +8 -2
  55. package/src/modes/shared.ts +2 -2
  56. package/src/modes/types.ts +1 -0
  57. package/src/modes/utils/ui-helpers.ts +1 -0
  58. package/src/prompts/system/system-prompt.md +5 -1
  59. package/src/prompts/tools/bash.md +16 -1
  60. package/src/prompts/tools/cancel-job.md +1 -1
  61. package/src/prompts/tools/chunk-edit.md +191 -163
  62. package/src/prompts/tools/hashline.md +11 -11
  63. package/src/prompts/tools/patch.md +10 -5
  64. package/src/prompts/tools/{await.md → poll.md} +1 -1
  65. package/src/prompts/tools/read-chunk.md +12 -3
  66. package/src/prompts/tools/read.md +9 -0
  67. package/src/prompts/tools/task.md +2 -2
  68. package/src/prompts/tools/vim.md +98 -0
  69. package/src/prompts/tools/write.md +1 -0
  70. package/src/sdk.ts +758 -725
  71. package/src/session/agent-session.ts +187 -40
  72. package/src/session/session-manager.ts +50 -4
  73. package/src/slash-commands/builtin-registry.ts +17 -0
  74. package/src/task/executor.ts +9 -5
  75. package/src/task/index.ts +3 -5
  76. package/src/task/types.ts +2 -2
  77. package/src/tools/bash.ts +240 -57
  78. package/src/tools/cancel-job.ts +2 -1
  79. package/src/tools/find.ts +5 -2
  80. package/src/tools/grep.ts +77 -8
  81. package/src/tools/index.ts +48 -19
  82. package/src/tools/inspect-image.ts +1 -1
  83. package/src/tools/{await-tool.ts → poll-tool.ts} +38 -31
  84. package/src/tools/python.ts +293 -278
  85. package/src/tools/read.ts +218 -1
  86. package/src/tools/sqlite-reader.ts +623 -0
  87. package/src/tools/submit-result.ts +5 -2
  88. package/src/tools/todo-write.ts +8 -2
  89. package/src/tools/vim.ts +966 -0
  90. package/src/tools/write.ts +187 -1
  91. package/src/utils/commit-message-generator.ts +1 -0
  92. package/src/utils/edit-mode.ts +2 -1
  93. package/src/utils/git.ts +24 -1
  94. package/src/utils/session-color.ts +55 -0
  95. package/src/utils/title-generator.ts +16 -7
  96. package/src/vim/buffer.ts +309 -0
  97. package/src/vim/commands.ts +382 -0
  98. package/src/vim/engine.ts +2426 -0
  99. package/src/vim/parser.ts +151 -0
  100. package/src/vim/render.ts +252 -0
  101. package/src/vim/types.ts +197 -0
@@ -28,11 +28,21 @@ import {
28
28
  import { isRecord, logger } from "@oh-my-pi/pi-utils";
29
29
  import { type Static, Type } from "@sinclair/typebox";
30
30
  import { type ConfigError, ConfigFile } from "../config";
31
- import { parseModelString } from "../config/model-resolver";
31
+ import { parseModelString, resolveProviderModelReference } 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 {
@@ -150,6 +160,7 @@ const OpenAICompatSchema = Type.Object({
150
160
  vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema),
151
161
  extraBody: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
152
162
  supportsStrictMode: Type.Optional(Type.Boolean()),
163
+ toolStrictMode: Type.Optional(Type.Union([Type.Literal("all_strict"), Type.Literal("none")])),
153
164
  });
154
165
 
155
166
  const EffortSchema = Type.Union([
@@ -263,8 +274,14 @@ const ProviderConfigSchema = Type.Object({
263
274
  modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)),
264
275
  });
265
276
 
277
+ const EquivalenceConfigSchema = Type.Object({
278
+ overrides: Type.Optional(Type.Record(Type.String(), Type.String({ minLength: 1 }))),
279
+ exclude: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
280
+ });
281
+
266
282
  const ModelsConfigSchema = Type.Object({
267
- providers: Type.Record(Type.String(), ProviderConfigSchema),
283
+ providers: Type.Optional(Type.Record(Type.String(), ProviderConfigSchema)),
284
+ equivalence: Type.Optional(EquivalenceConfigSchema),
268
285
  });
269
286
 
270
287
  type ModelsConfig = Static<typeof ModelsConfigSchema>;
@@ -356,7 +373,7 @@ function validateProviderConfiguration(
356
373
  export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsConfigSchema).withValidation(
357
374
  "models",
358
375
  config => {
359
- for (const [providerName, providerConfig] of Object.entries(config.providers)) {
376
+ for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
360
377
  validateProviderConfiguration(
361
378
  providerName,
362
379
  {
@@ -405,6 +422,11 @@ export interface ProviderDiscoveryState {
405
422
  error?: string;
406
423
  }
407
424
 
425
+ export interface CanonicalModelQueryOptions {
426
+ availableOnly?: boolean;
427
+ candidates?: readonly Model<Api>[];
428
+ }
429
+
408
430
  /** Result of loading custom models from models.json */
409
431
  interface CustomModelsResult {
410
432
  models?: CustomModelOverlay[];
@@ -413,6 +435,7 @@ interface CustomModelsResult {
413
435
  keylessProviders?: Set<string>;
414
436
  discoverableProviders?: DiscoveryProviderConfig[];
415
437
  configuredProviders?: Set<string>;
438
+ equivalence?: ModelEquivalenceConfig;
416
439
  error?: ConfigError;
417
440
  found: boolean;
418
441
  }
@@ -698,31 +721,6 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
698
721
  } as Model<Api>);
699
722
  }
700
723
 
701
- function buildCustomModel(
702
- providerName: string,
703
- providerBaseUrl: string,
704
- providerApi: Api | undefined,
705
- providerHeaders: Record<string, string> | undefined,
706
- providerApiKey: string | undefined,
707
- authHeader: boolean | undefined,
708
- providerCompat: Model<Api>["compat"] | undefined,
709
- modelDef: CustomModelDefinitionLike,
710
- options: CustomModelBuildOptions,
711
- ): Model<Api> | undefined {
712
- const model = buildCustomModelOverlay(
713
- providerName,
714
- providerBaseUrl,
715
- providerApi,
716
- providerHeaders,
717
- providerApiKey,
718
- authHeader,
719
- providerCompat,
720
- modelDef,
721
- );
722
- if (!model) return undefined;
723
- return finalizeCustomModel(model, options);
724
- }
725
-
726
724
  function normalizeSuppressedSelector(selector: string): string {
727
725
  const trimmed = selector.trim();
728
726
  if (!trimmed) return trimmed;
@@ -739,17 +737,27 @@ function getDisabledProviderIdsFromSettings(): Set<string> {
739
737
  }
740
738
  }
741
739
 
740
+ function getConfiguredProviderOrderFromSettings(): string[] {
741
+ try {
742
+ return settings.get("modelProviderOrder");
743
+ } catch {
744
+ return [];
745
+ }
746
+ }
747
+
742
748
  /**
743
749
  * Model registry - loads and manages models, resolves API keys via AuthStorage.
744
750
  */
745
751
  export class ModelRegistry {
746
752
  #models: Model<Api>[] = [];
753
+ #canonicalIndex: CanonicalModelIndex = { records: [], byId: new Map(), bySelector: new Map() };
747
754
  #customProviderApiKeys: Map<string, string> = new Map();
748
755
  #keylessProviders: Set<string> = new Set();
749
756
  #discoverableProviders: DiscoveryProviderConfig[] = [];
750
757
  #customModelOverlays: CustomModelOverlay[] = [];
751
758
  #providerOverrides: Map<string, ProviderOverride> = new Map();
752
759
  #modelOverrides: Map<string, Map<string, ModelOverride>> = new Map();
760
+ #equivalenceConfig: ModelEquivalenceConfig | undefined;
753
761
  #configError: ConfigError | undefined = undefined;
754
762
  #modelsConfigFile: ConfigFile<ModelsConfig>;
755
763
  #registeredProviderSources: Set<string> = new Set();
@@ -758,6 +766,12 @@ export class ModelRegistry {
758
766
  #suppressedSelectors: Map<string, number> = new Map();
759
767
  #backgroundRefresh?: Promise<void>;
760
768
  #lastDiscoveryWarnings: Map<string, string> = new Map();
769
+ // Runtime extension model overlays — persist across refresh() cycles so that
770
+ // models registered by extensions survive the model selector's offline reload.
771
+ #runtimeModelOverlays: CustomModelOverlay[] = [];
772
+ #runtimeProviderApiKeys: Map<string, string> = new Map();
773
+ #runtimeProvidersBySource: Map<string, Set<string>> = new Map();
774
+ #runtimeProviderSourceByName: Map<string, string> = new Map();
761
775
 
762
776
  /**
763
777
  * @param authStorage - Auth storage for API key resolution
@@ -822,8 +836,14 @@ export class ModelRegistry {
822
836
  this.#customProviderApiKeys.clear();
823
837
  this.#keylessProviders.clear();
824
838
  this.#discoverableProviders = [];
839
+ // Restore runtime API keys before #loadModels — survives because
840
+ // #loadModels only calls .set() on #customProviderApiKeys, never reassigns it.
841
+ for (const [k, v] of this.#runtimeProviderApiKeys) {
842
+ this.#customProviderApiKeys.set(k, v);
843
+ }
825
844
  this.#providerOverrides.clear();
826
845
  this.#modelOverrides.clear();
846
+ this.#equivalenceConfig = undefined;
827
847
  this.#configError = undefined;
828
848
  this.#providerDiscoveryStates.clear();
829
849
  this.#loadModels();
@@ -845,6 +865,7 @@ export class ModelRegistry {
845
865
  keylessProviders = new Set(),
846
866
  discoverableProviders = [],
847
867
  configuredProviders = new Set(),
868
+ equivalence,
848
869
  error: configError,
849
870
  } = this.#loadCustomModels();
850
871
  this.#configError = configError;
@@ -853,14 +874,18 @@ export class ModelRegistry {
853
874
  this.#customModelOverlays = customModels;
854
875
  this.#providerOverrides = overrides;
855
876
  this.#modelOverrides = modelOverrides;
877
+ this.#equivalenceConfig = equivalence;
856
878
 
857
879
  this.#addImplicitDiscoverableProviders(configuredProviders);
858
880
  const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
859
881
  const cachedDiscoveries = this.#applyHardcodedModelPolicies(this.#loadCachedDiscoverableModels());
860
882
  const resolvedDefaults = this.#mergeResolvedModels(builtInModels, cachedDiscoveries);
861
- const combined = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
883
+ const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
884
+ // Merge runtime extension models so they survive refresh() cycles
885
+ const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
862
886
 
863
887
  this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
888
+ this.#rebuildCanonicalIndex();
864
889
  }
865
890
 
866
891
  /** Load built-in models, applying provider-level overrides only.
@@ -1045,9 +1070,10 @@ export class ModelRegistry {
1045
1070
  const allModelOverrides = new Map<string, Map<string, ModelOverride>>();
1046
1071
  const keylessProviders = new Set<string>();
1047
1072
  const discoverableProviders: DiscoveryProviderConfig[] = [];
1048
- const configuredProviders = new Set(Object.keys(value.providers));
1073
+ const providerEntries = Object.entries(value.providers ?? {});
1074
+ const configuredProviders = new Set(Object.keys(value.providers ?? {}));
1049
1075
 
1050
- for (const [providerName, providerConfig] of Object.entries(value.providers)) {
1076
+ for (const [providerName, providerConfig] of providerEntries) {
1051
1077
  // Always set overrides when baseUrl/headers/apiKey/compat are present
1052
1078
  if (providerConfig.baseUrl || providerConfig.headers || providerConfig.apiKey || providerConfig.compat) {
1053
1079
  overrides.set(providerName, {
@@ -1097,6 +1123,7 @@ export class ModelRegistry {
1097
1123
  keylessProviders,
1098
1124
  discoverableProviders,
1099
1125
  configuredProviders,
1126
+ equivalence: value.equivalence,
1100
1127
  found: true,
1101
1128
  };
1102
1129
  }
@@ -1145,8 +1172,11 @@ export class ModelRegistry {
1145
1172
  }),
1146
1173
  );
1147
1174
  const resolved = this.#mergeResolvedModels(this.#models, discoveredModels);
1148
- const combined = this.#mergeCustomModels(resolved, this.#customModelOverlays);
1175
+ const withConfigModels = this.#mergeCustomModels(resolved, this.#customModelOverlays);
1176
+ // Merge runtime extension models so they survive online discovery completion
1177
+ const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
1149
1178
  this.#models = this.#applyModelOverrides(combined, this.#modelOverrides);
1179
+ this.#rebuildCanonicalIndex();
1150
1180
  }
1151
1181
 
1152
1182
  async #discoverProviderModels(
@@ -1653,10 +1683,14 @@ export class ModelRegistry {
1653
1683
  });
1654
1684
  }
1655
1685
 
1686
+ #rebuildCanonicalIndex(): void {
1687
+ this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig);
1688
+ }
1689
+
1656
1690
  #parseModels(config: ModelsConfig): CustomModelOverlay[] {
1657
1691
  const models: CustomModelOverlay[] = [];
1658
1692
 
1659
- for (const [providerName, providerConfig] of Object.entries(config.providers)) {
1693
+ for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
1660
1694
  const modelDefs = providerConfig.models ?? [];
1661
1695
  if (modelDefs.length === 0) continue; // Override-only, no custom models
1662
1696
  if (providerConfig.apiKey) {
@@ -1688,17 +1722,139 @@ export class ModelRegistry {
1688
1722
  return this.#models;
1689
1723
  }
1690
1724
 
1725
+ #isModelAvailable(model: Model<Api>): boolean {
1726
+ const disabledProviders = getDisabledProviderIdsFromSettings();
1727
+ return (
1728
+ !disabledProviders.has(model.provider) &&
1729
+ (this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider))
1730
+ );
1731
+ }
1732
+
1733
+ #filterCanonicalVariants(
1734
+ record: CanonicalModelRecord,
1735
+ options: CanonicalModelQueryOptions | undefined,
1736
+ ): CanonicalModelVariant[] {
1737
+ const candidateKeys = options?.candidates
1738
+ ? new Set(options.candidates.map(candidate => formatCanonicalVariantSelector(candidate)))
1739
+ : undefined;
1740
+ return record.variants.filter(variant => {
1741
+ if (candidateKeys && !candidateKeys.has(variant.selector)) {
1742
+ return false;
1743
+ }
1744
+ if (options?.availableOnly && !this.#isModelAvailable(variant.model)) {
1745
+ return false;
1746
+ }
1747
+ return true;
1748
+ });
1749
+ }
1750
+
1751
+ #providerRank(models: readonly Model<Api>[]): Map<string, number> {
1752
+ const configuredProviders = getConfiguredProviderOrderFromSettings();
1753
+ const result = new Map<string, number>();
1754
+ let nextRank = 0;
1755
+ for (const provider of configuredProviders) {
1756
+ const normalized = provider.trim().toLowerCase();
1757
+ if (!normalized || result.has(normalized)) {
1758
+ continue;
1759
+ }
1760
+ result.set(normalized, nextRank);
1761
+ nextRank += 1;
1762
+ }
1763
+ for (const model of models) {
1764
+ const normalized = model.provider.toLowerCase();
1765
+ if (result.has(normalized)) {
1766
+ continue;
1767
+ }
1768
+ result.set(normalized, nextRank);
1769
+ nextRank += 1;
1770
+ }
1771
+ return result;
1772
+ }
1773
+
1774
+ #resolveCanonicalVariant(
1775
+ variants: readonly CanonicalModelVariant[],
1776
+ allCandidates: readonly Model<Api>[],
1777
+ ): CanonicalModelVariant | undefined {
1778
+ if (variants.length === 0) {
1779
+ return undefined;
1780
+ }
1781
+ const providerRank = this.#providerRank(allCandidates);
1782
+ const modelOrder = new Map<string, number>();
1783
+ for (let index = 0; index < allCandidates.length; index += 1) {
1784
+ modelOrder.set(formatCanonicalVariantSelector(allCandidates[index]!), index);
1785
+ }
1786
+ const sourceRank: Record<CanonicalModelVariant["source"], number> = {
1787
+ override: 1,
1788
+ bundled: 1,
1789
+ heuristic: 2,
1790
+ fallback: 3,
1791
+ };
1792
+ return [...variants].sort((left, right) => {
1793
+ const leftProviderRank = providerRank.get(left.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
1794
+ const rightProviderRank = providerRank.get(right.model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
1795
+ if (leftProviderRank !== rightProviderRank) {
1796
+ return leftProviderRank - rightProviderRank;
1797
+ }
1798
+ const leftExact = left.model.id === left.canonicalId ? 0 : 1;
1799
+ const rightExact = right.model.id === right.canonicalId ? 0 : 1;
1800
+ if (leftExact !== rightExact) {
1801
+ return leftExact - rightExact;
1802
+ }
1803
+ if (sourceRank[left.source] !== sourceRank[right.source]) {
1804
+ return sourceRank[left.source] - sourceRank[right.source];
1805
+ }
1806
+ if (left.model.id.length !== right.model.id.length) {
1807
+ return left.model.id.length - right.model.id.length;
1808
+ }
1809
+ const leftOrder = modelOrder.get(left.selector) ?? Number.MAX_SAFE_INTEGER;
1810
+ const rightOrder = modelOrder.get(right.selector) ?? Number.MAX_SAFE_INTEGER;
1811
+ return leftOrder - rightOrder;
1812
+ })[0];
1813
+ }
1814
+
1815
+ getCanonicalModels(options?: CanonicalModelQueryOptions): CanonicalModelRecord[] {
1816
+ const records: CanonicalModelRecord[] = [];
1817
+ for (const record of this.#canonicalIndex.records) {
1818
+ const variants = this.#filterCanonicalVariants(record, options);
1819
+ if (variants.length === 0) {
1820
+ continue;
1821
+ }
1822
+ records.push({
1823
+ id: record.id,
1824
+ name: record.name,
1825
+ variants,
1826
+ });
1827
+ }
1828
+ return records;
1829
+ }
1830
+
1831
+ getCanonicalVariants(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[] {
1832
+ const record = this.#canonicalIndex.byId.get(canonicalId.trim().toLowerCase());
1833
+ if (!record) {
1834
+ return [];
1835
+ }
1836
+ return this.#filterCanonicalVariants(record, options);
1837
+ }
1838
+
1839
+ resolveCanonicalModel(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined {
1840
+ const variants = this.getCanonicalVariants(canonicalId, options);
1841
+ if (variants.length === 0) {
1842
+ return undefined;
1843
+ }
1844
+ const candidates = options?.candidates ?? (options?.availableOnly ? this.getAvailable() : this.getAll());
1845
+ return this.#resolveCanonicalVariant(variants, candidates)?.model;
1846
+ }
1847
+
1848
+ getCanonicalId(model: Model<Api>): string | undefined {
1849
+ return this.#canonicalIndex.bySelector.get(formatCanonicalVariantSelector(model).toLowerCase());
1850
+ }
1851
+
1691
1852
  /**
1692
1853
  * Get only models that have auth configured.
1693
1854
  * This is a fast check that doesn't refresh OAuth tokens.
1694
1855
  */
1695
1856
  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
- );
1857
+ return this.#models.filter(model => this.#isModelAvailable(model));
1702
1858
  }
1703
1859
 
1704
1860
  getDiscoverableProviders(): string[] {
@@ -1716,7 +1872,7 @@ export class ModelRegistry {
1716
1872
  * Find a model by provider and ID.
1717
1873
  */
1718
1874
  find(provider: string, modelId: string): Model<Api> | undefined {
1719
- return this.#models.find(m => m.provider === provider && m.id === modelId);
1875
+ return resolveProviderModelReference(provider, modelId, this.#models);
1720
1876
  }
1721
1877
 
1722
1878
  /**
@@ -1766,6 +1922,21 @@ export class ModelRegistry {
1766
1922
  clearSourceRegistrations(sourceId: string): void {
1767
1923
  unregisterCustomApis(sourceId);
1768
1924
  unregisterOAuthProviders(sourceId);
1925
+ const sourceProviders = this.#runtimeProvidersBySource.get(sourceId);
1926
+ if (!sourceProviders || sourceProviders.size === 0) {
1927
+ return;
1928
+ }
1929
+ this.#runtimeProvidersBySource.delete(sourceId);
1930
+ for (const providerName of sourceProviders) {
1931
+ if (this.#runtimeProviderSourceByName.get(providerName) !== sourceId) {
1932
+ continue;
1933
+ }
1934
+ this.#runtimeProviderSourceByName.delete(providerName);
1935
+ this.#runtimeProviderApiKeys.delete(providerName);
1936
+ this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
1937
+ }
1938
+ this.#reloadStaticModels();
1939
+ this.#rebuildCanonicalIndex();
1769
1940
  }
1770
1941
 
1771
1942
  /**
@@ -1824,15 +1995,30 @@ export class ModelRegistry {
1824
1995
 
1825
1996
  if (sourceId) {
1826
1997
  this.#registeredProviderSources.add(sourceId);
1998
+ const previousSourceId = this.#runtimeProviderSourceByName.get(providerName);
1999
+ if (previousSourceId && previousSourceId !== sourceId) {
2000
+ const previousProviders = this.#runtimeProvidersBySource.get(previousSourceId);
2001
+ previousProviders?.delete(providerName);
2002
+ if (previousProviders && previousProviders.size === 0) {
2003
+ this.#runtimeProvidersBySource.delete(previousSourceId);
2004
+ }
2005
+ }
2006
+ const sourceProviders = this.#runtimeProvidersBySource.get(sourceId) ?? new Set<string>();
2007
+ sourceProviders.add(providerName);
2008
+ this.#runtimeProvidersBySource.set(sourceId, sourceProviders);
2009
+ this.#runtimeProviderSourceByName.set(providerName, sourceId);
1827
2010
  }
1828
2011
  if (config.apiKey) {
1829
2012
  this.#customProviderApiKeys.set(providerName, config.apiKey);
2013
+ // Persist runtime API keys so they survive #reloadStaticModels() cycles
2014
+ this.#runtimeProviderApiKeys.set(providerName, config.apiKey);
1830
2015
  }
1831
2016
 
1832
2017
  if (config.models && config.models.length > 0) {
1833
- const nextModels = this.#models.filter(m => m.provider !== providerName);
2018
+ // Build model overlays that persist across refresh() cycles
2019
+ const newOverlays: CustomModelOverlay[] = [];
1834
2020
  for (const modelDef of config.models) {
1835
- const model = buildCustomModel(
2021
+ const overlay = buildCustomModelOverlay(
1836
2022
  providerName,
1837
2023
  config.baseUrl!,
1838
2024
  config.api,
@@ -1841,23 +2027,33 @@ export class ModelRegistry {
1841
2027
  config.authHeader,
1842
2028
  config.compat,
1843
2029
  modelDef as CustomModelDefinitionLike,
1844
- { useDefaults: true },
1845
2030
  );
1846
- if (!model) {
2031
+ if (!overlay) {
1847
2032
  throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`);
1848
2033
  }
1849
- nextModels.push(model);
2034
+ newOverlays.push(overlay);
2035
+ }
2036
+ // Store as runtime overlays so they survive #reloadStaticModels()
2037
+ this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(m => m.provider !== providerName);
2038
+ this.#runtimeModelOverlays.push(...newOverlays);
2039
+
2040
+ // Also update #models immediately for the current cycle
2041
+ const nextModels = this.#models.filter(m => m.provider !== providerName);
2042
+ for (const overlay of newOverlays) {
2043
+ nextModels.push(finalizeCustomModel(overlay, { useDefaults: true }));
1850
2044
  }
1851
2045
 
1852
2046
  if (config.oauth?.modifyModels) {
1853
2047
  const credential = this.authStorage.getOAuthCredential(providerName);
1854
2048
  if (credential) {
1855
2049
  this.#models = config.oauth.modifyModels(nextModels, credential);
2050
+ this.#rebuildCanonicalIndex();
1856
2051
  return;
1857
2052
  }
1858
2053
  }
1859
2054
 
1860
2055
  this.#models = nextModels;
2056
+ this.#rebuildCanonicalIndex();
1861
2057
  return;
1862
2058
  }
1863
2059
 
@@ -1870,6 +2066,7 @@ export class ModelRegistry {
1870
2066
  headers: config.headers ? { ...m.headers, ...config.headers } : m.headers,
1871
2067
  };
1872
2068
  });
2069
+ this.#rebuildCanonicalIndex();
1873
2070
  }
1874
2071
  }
1875
2072