* List available models with their metadata. * * If an `onListModels` handler was provided in the client options, * it is called instead of querying the CLI server. * * Results are cached after the first successful call to avoid rate limiting. * The cache is cleared whe
()
| 1751 | * @throws Error if not connected (when no custom handler is set) |
| 1752 | */ |
| 1753 | async listModels(): Promise<ModelInfo[]> { |
| 1754 | // Use promise-based locking to prevent race condition with concurrent calls |
| 1755 | await this.modelsCacheLock; |
| 1756 | |
| 1757 | let resolveLock: () => void; |
| 1758 | this.modelsCacheLock = new Promise((resolve) => { |
| 1759 | resolveLock = resolve; |
| 1760 | }); |
| 1761 | |
| 1762 | try { |
| 1763 | // Check cache (already inside lock) |
| 1764 | if (this.modelsCache !== null) { |
| 1765 | return [...this.modelsCache]; // Return a copy to prevent cache mutation |
| 1766 | } |
| 1767 | |
| 1768 | let models: ModelInfo[]; |
| 1769 | if (this.onListModels) { |
| 1770 | // Use custom handler instead of CLI RPC |
| 1771 | models = await this.onListModels(); |
| 1772 | } else { |
| 1773 | if (!this.connection) { |
| 1774 | throw new Error("Client not connected"); |
| 1775 | } |
| 1776 | // Cache miss - fetch from backend while holding lock |
| 1777 | const result = await this.connection.sendRequest("models.list", {}); |
| 1778 | const response = result as { models: ModelInfo[] }; |
| 1779 | models = response.models; |
| 1780 | |
| 1781 | // Normalize model capabilities — some models (e.g. embedding models) |
| 1782 | // may omit 'supports' or 'limits' in their capabilities. |
| 1783 | for (const model of models) { |
| 1784 | // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 1785 | const m = model as any; |
| 1786 | if (!m.capabilities) { |
| 1787 | m.capabilities = { |
| 1788 | supports: {}, |
| 1789 | limits: { max_context_window_tokens: 0 }, |
| 1790 | }; |
| 1791 | } else { |
| 1792 | if (!m.capabilities.supports) m.capabilities.supports = {}; |
| 1793 | if (!m.capabilities.limits) { |
| 1794 | m.capabilities.limits = { max_context_window_tokens: 0 }; |
| 1795 | } else if (m.capabilities.limits.max_context_window_tokens === undefined) { |
| 1796 | m.capabilities.limits.max_context_window_tokens = 0; |
| 1797 | } |
| 1798 | } |
| 1799 | } |
| 1800 | } |
| 1801 | |
| 1802 | // Update cache before releasing lock (copy to prevent external mutation) |
| 1803 | this.modelsCache = [...models]; |
| 1804 | |
| 1805 | return [...models]; // Return a copy to prevent cache mutation |
| 1806 | } finally { |
| 1807 | resolveLock!(); |
| 1808 | } |
| 1809 | } |
| 1810 |
no test coverage detected